Skip to content

kups.core.neighborlist

Neighbor list construction and edge representations for molecular systems.

This module provides multiple neighbor list algorithms for finding interacting pairs of particles within cutoff distances, with different performance and accuracy trade-offs.

Call Contract

Neighbor lists are called as neighborlist(keys, systems, *, queries=None, queried_keys=None). keys is the self-graph/output table. Use keyword-only queries only for true bipartite queries. Use keyword-only queried_keys only for self-graph updates; it is mutually exclusive with queries and names affected keys ids after the caller has already written updated particle data into keys.

Core Components

  • Edges: Represents connections between particles with periodic shifts
  • NeighborList: Protocol for neighbor search implementations
  • Pipeline: Selector → mask sequence → compactor → postprocessors

Neighbor List Implementations

Primary Implementations

  1. CellListNeighborList (Recommended when cutoff << box size)

    • O(N) complexity using spatial hashing
    • Best when cutoff / box_size < 0.3 (cutoff much smaller than box)
    • Honors the cell's per-axis periodic mask (bulk and bounded non-periodic)
  2. DenseNearestNeighborList

    • O(N²/K) complexity (K = number of systems)
    • Best when cutoff / box_size ~ 1 (cutoff comparable to box)
  3. AllDenseNearestNeighborList

    • O(N²) complexity across all systems
    • Only for single-system simulations or testing
    • Crosses system boundaries (use with caution!)

Refinement Implementations

These let one expensive base neighbor list be shared across multiple potentials.

  1. RefineMaskNeighborList: apply different inclusion/exclusion masks to precomputed edges.
  2. RefineCutoffNeighborList: refine precomputed edges with new cutoff distances.

Cutoff-Free Implementations

These cover non-cutoff cases under the same NeighborList[D] protocol.

  1. EmptyNeighborList: emits a zero-row Edges[D] for point-cloud constructions.
  2. FixedEdgesNeighborList: stores fixed edge topology for bonded edge sets supplied by the state and computes current periodic shifts during calls. Affected self-graph calls use queried_keys and return only rows touched by those affected keys ids.

Pipeline Primitives

Every neighbor list above is a Pipeline of a CandidateSelector, a tuple of Mask criteria, a Compactor, and zero or more Postprocessor transforms. Users wanting custom behavior can compose their own pipeline directly.

AdaptiveNeighborList

Bases: NeighborList[Literal[2]]

Neighbor list that dispatches to the cheapest backing implementation.

Holds (implementation, cost) pairs and, on each call, picks the implementation whose :class:NeighborListCost is smallest for that call's counts. Augment by appending pairs to :attr:implementations; the seeded implementations share the UniversalNeighborlistParameters capacities, so growing one grows the shared state.

Attributes:

Name Type Description
implementations tuple[NeighborListCandidate, ...]

Candidate :class:NeighborListCandidate pairs. Ties resolve to the earlier entry.

Example
nl = AdaptiveNeighborList.from_state(state, cutoffs)
edges = nl(particles, systems)
Source code in src/kups/core/neighborlist/adaptive.py
@dataclass
class AdaptiveNeighborList(NeighborList[Literal[2]]):
    """Neighbor list that dispatches to the cheapest backing implementation.

    Holds ``(implementation, cost)`` pairs and, on each call, picks the
    implementation whose :class:`NeighborListCost` is smallest for that call's
    counts. Augment by appending pairs to :attr:`implementations`; the seeded
    implementations share the ``UniversalNeighborlistParameters`` capacities, so
    growing one grows the shared state.

    Attributes:
        implementations: Candidate :class:`NeighborListCandidate` pairs. Ties
            resolve to the earlier entry.

    Example:
        ```python
        nl = AdaptiveNeighborList.from_state(state, cutoffs)
        edges = nl(particles, systems)
        ```
    """

    implementations: tuple[NeighborListCandidate, ...]

    @classmethod
    def new[S](
        cls,
        state: S,
        lens: Lens[S, IsUniversalNeighborlistParams],
        cutoffs: Table[SystemId, Array],
    ) -> AdaptiveNeighborList:
        """Seed the library implementations with their default cost guesses.

        Args:
            state: Object exposing ``UniversalNeighborlistParameters`` via ``lens``.
            lens: Lens focusing the shared ``IsUniversalNeighborlistParams``.
            cutoffs: Per-system cutoffs bound onto each implementation.

        Returns:
            An ``AdaptiveNeighborList`` over dense, cell-list, and all-dense.
        """
        return cls(
            (
                NeighborListCandidate(
                    DenseNearestNeighborList.new(state, lens, cutoffs), dense_cost
                ),
                NeighborListCandidate(
                    CellListNeighborList.new(state, lens, cutoffs), cell_list_cost
                ),
                NeighborListCandidate(
                    AllDenseNearestNeighborList.new(state, lens, cutoffs),
                    all_dense_cost,
                ),
            )
        )

    @classmethod
    def from_state(
        cls,
        state: IsNeighborListState[IsUniversalNeighborlistParams],
        cutoffs: Table[SystemId, Array],
    ) -> AdaptiveNeighborList:
        """Seed from a state exposing ``neighborlist_params``."""
        return cls.new(state, lens(lambda s: s.neighborlist_params), cutoffs)

    def _choose(self, num_particles: int, num_systems: int) -> NeighborList[Literal[2]]:
        """Return the cheapest implementation for the given counts."""
        return min(
            self.implementations,
            key=lambda candidate: candidate.cost(num_particles, num_systems),
        ).neighborlist

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[Literal[2]]: ...
    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]: ...
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        nl = self._choose(keys.size, systems.size)
        if queries is not None:
            return nl(keys, systems, queries=queries)
        return nl(keys, systems, queried_keys=queried_keys)

from_state(state, cutoffs) classmethod

Seed from a state exposing neighborlist_params.

Source code in src/kups/core/neighborlist/adaptive.py
@classmethod
def from_state(
    cls,
    state: IsNeighborListState[IsUniversalNeighborlistParams],
    cutoffs: Table[SystemId, Array],
) -> AdaptiveNeighborList:
    """Seed from a state exposing ``neighborlist_params``."""
    return cls.new(state, lens(lambda s: s.neighborlist_params), cutoffs)

new(state, lens, cutoffs) classmethod

Seed the library implementations with their default cost guesses.

Parameters:

Name Type Description Default
state S

Object exposing UniversalNeighborlistParameters via lens.

required
lens Lens[S, IsUniversalNeighborlistParams]

Lens focusing the shared IsUniversalNeighborlistParams.

required
cutoffs Table[SystemId, Array]

Per-system cutoffs bound onto each implementation.

required

Returns:

Type Description
AdaptiveNeighborList

An AdaptiveNeighborList over dense, cell-list, and all-dense.

Source code in src/kups/core/neighborlist/adaptive.py
@classmethod
def new[S](
    cls,
    state: S,
    lens: Lens[S, IsUniversalNeighborlistParams],
    cutoffs: Table[SystemId, Array],
) -> AdaptiveNeighborList:
    """Seed the library implementations with their default cost guesses.

    Args:
        state: Object exposing ``UniversalNeighborlistParameters`` via ``lens``.
        lens: Lens focusing the shared ``IsUniversalNeighborlistParams``.
        cutoffs: Per-system cutoffs bound onto each implementation.

    Returns:
        An ``AdaptiveNeighborList`` over dense, cell-list, and all-dense.
    """
    return cls(
        (
            NeighborListCandidate(
                DenseNearestNeighborList.new(state, lens, cutoffs), dense_cost
            ),
            NeighborListCandidate(
                CellListNeighborList.new(state, lens, cutoffs), cell_list_cost
            ),
            NeighborListCandidate(
                AllDenseNearestNeighborList.new(state, lens, cutoffs),
                all_dense_cost,
            ),
        )
    )

AllDenseNearestNeighborList

Dense O(N²) neighbor list considering all pairs across all systems.

This implementation generates all possible particle pairs without spatial optimization. It is only suitable for very small systems or testing.

Warning: This crosses system boundaries! Only use for single-system simulations. For multiple systems, use DenseNearestNeighborList instead.

Complexity: O(N²) where N is the total number of particles across all systems.

Attributes:

Name Type Description
avg_edges Capacity[int]

Capacity manager for edge array.

avg_image_candidates Capacity[int]

Capacity manager for image candidate pairs.

cutoffs Table[SystemId, Array]

Per-system cutoff distances used by this neighbor list.

Example
# Construct from state and a lens to the neighbor list parameters:
nl = AllDenseNearestNeighborList.new(state, lens(lambda s: s.nl_params), cutoffs)

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

edges = nl(particles, systems)
Source code in src/kups/core/neighborlist/all_dense.py
@dataclass
class AllDenseNearestNeighborList:
    """Dense O(N²) neighbor list considering all pairs across all systems.

    This implementation generates all possible particle pairs without spatial
    optimization. It is only suitable for very small systems or testing.

    **Warning**: This crosses system boundaries! Only use for single-system
    simulations. For multiple systems, use
    [DenseNearestNeighborList][kups.core.neighborlist.DenseNearestNeighborList]
    instead.

    Complexity: O(N²) where N is the total number of particles across all systems.

    Attributes:
        avg_edges: Capacity manager for edge array.
        avg_image_candidates: Capacity manager for image candidate pairs.
        cutoffs: Per-system cutoff distances used by this neighbor list.

    Example:
        ```python
        # Construct from state and a lens to the neighbor list parameters:
        nl = AllDenseNearestNeighborList.new(state, lens(lambda s: s.nl_params), cutoffs)

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

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

    avg_edges: Capacity[int]
    avg_image_candidates: Capacity[int]
    cutoffs: Table[SystemId, Array]

    @classmethod
    def new[S](
        cls,
        state: S,
        lens: Lens[S, IsAllDenseNeighborListParams],
        cutoffs: Table[SystemId, Array],
    ) -> AllDenseNearestNeighborList:
        params = lens.get(state)
        return AllDenseNearestNeighborList(
            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),
            ),
            cutoffs=cutoffs,
        )

    @classmethod
    def from_state(
        cls,
        state: IsNeighborListState[IsAllDenseNeighborListParams],
        cutoffs: Table[SystemId, Array],
    ) -> AllDenseNearestNeighborList:
        return cls.new(state, lens(lambda s: s.neighborlist_params), cutoffs)

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[Literal[2]]: ...
    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]: ...
    @jit
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        if keys.data.inclusion.num_labels >= 2:
            logging.warning(
                "AllDenseNearestNeighborList is intended for single-system simulations. "
                "Performance may be degraded when using multiple systems. "
                "Consider using DenseNearestNeighborList or CellListNeighborList instead."
            )
        query_size = (
            queried_keys.size
            if queried_keys is not None
            else (queries.size if queries is not None else keys.size)
        )
        cutoffs = Table.broadcast_to(self.cutoffs, systems)
        pipeline = Pipeline[Literal[2]](
            selector=AllDenseSelector(
                cutoffs=cutoffs,
                max_image_candidates=self.avg_image_candidates.multiply(query_size),
            ),
            masks=(
                InBoundsMask(),
                InclusionMatchMask(),
                QueriedKeysDedupMask(),
                DistanceCutoffMask(cutoffs=cutoffs),
                ExclusionMask(),
            ),
            compactor=ReduceCompactor(avg_edges=self.avg_edges.multiply(query_size)),
            postprocessors=(MirrorPairEdges(),),
        )
        if queries is not None:
            return pipeline(keys, systems, queries=queries)
        return pipeline(keys, systems, queried_keys=queried_keys)

AllDenseSelector

Selector that emits every (i, j) pair across all systems.

Source code in src/kups/core/neighborlist/all_dense.py
@dataclass
class AllDenseSelector:
    """Selector that emits every ``(i, j)`` pair across all systems."""

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

    def __call__(self, ctx: PipelineContext) -> CandidateBatch[Literal[2]]:
        query = ctx.query_table
        candidates = _all_subselect(ctx.keys, query, ctx.systems)
        candidates = lift_query_candidates(candidates, ctx)
        return replicate_for_images(
            candidates,
            ctx.keys,
            ctx.edge_query_table,
            ctx.systems,
            self.cutoffs,
            self.max_image_candidates,
        )

CandidateBatch

Candidate set of degree D carried through the pipeline.

Reuses Edges[D] for the (indices, shifts) layout (indices shape (n, D), shifts shape (n, D-1, 3)); adds the is_minimum_image flag that ExclusionMask needs to keep non-minimum periodic copies of excluded pairs.

Attributes:

Name Type Description
edges Edges[D]

Candidate edges (indices + fractional shifts). The first column is keyed by edges.indices.keys.

is_minimum_image Array

(n,) bool — True where the candidate's shift equals the minimum-image shift; False for non-MIC replicated copies emitted by selectors that handle PBC image expansion.

query_keys tuple[ParticleId, ...] | None

Pair-specific key vocabulary for the second edge column. None means it uses edges.indices.keys.

Source code in src/kups/core/neighborlist/types.py
@dataclass
class CandidateBatch[D: int]:
    """Candidate set of degree ``D`` carried through the pipeline.

    Reuses [`Edges[D]`][kups.core.neighborlist.edges.Edges] for the
    `(indices, shifts)` layout (`indices` shape `(n, D)`,
    `shifts` shape `(n, D-1, 3)`); adds the
    ``is_minimum_image`` flag that
    [`ExclusionMask`][kups.core.neighborlist.masks.ExclusionMask] needs to
    keep non-minimum periodic copies of excluded pairs.

    Attributes:
        edges: Candidate edges (indices + fractional shifts). The first column
            is keyed by ``edges.indices.keys``.
        is_minimum_image: ``(n,)`` bool — True where the candidate's shift
            equals the minimum-image shift; False for non-MIC replicated
            copies emitted by selectors that handle PBC image expansion.
        query_keys: Pair-specific key vocabulary for the second edge column.
            ``None`` means it uses ``edges.indices.keys``.
    """

    edges: Edges[D]
    is_minimum_image: Array
    query_keys: tuple[ParticleId, ...] | None = field(default=None, static=True)

    @property
    def key_idx(self) -> Index[ParticleId]:
        """Pair-specific: key-side index of shape ``(n,)``. Only meaningful for ``D == 2``."""
        return self.edges.indices[:, 0]

    @property
    def query_idx(self) -> Index[ParticleId]:
        """Pair-specific: query-side index of shape ``(n,)``. Only meaningful for ``D == 2``."""
        return Index(
            self.query_keys or self.edges.indices.keys,
            self.edges.indices.indices[:, 1],
        )

key_idx property

Pair-specific: key-side index of shape (n,). Only meaningful for D == 2.

query_idx property

Pair-specific: query-side index of shape (n,). Only meaningful for D == 2.

CandidateSelector

Bases: Protocol

Produces a CandidateBatch[D] from the pipeline context.

Owns all candidate-set construction, including any PBC image replication required when max(cutoff/perp_axis) > 0.5.

Source code in src/kups/core/neighborlist/types.py
class CandidateSelector[D: int](Protocol):
    """Produces a ``CandidateBatch[D]`` from the pipeline context.

    Owns all candidate-set construction, including any PBC image
    replication required when ``max(cutoff/perp_axis) > 0.5``.
    """

    def __call__(self, ctx: PipelineContext) -> CandidateBatch[D]: ...

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.

cutoffs Table[SystemId, Array]

Per-system cutoff distances used by this neighbor list.

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), cutoffs)

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

edges = nl(particles, systems)
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.
        cutoffs: Per-system cutoff distances used by this neighbor list.

    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), cutoffs)

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

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

    avg_candidates: Capacity[int]
    avg_edges: Capacity[int]
    cells: Capacity[int]
    avg_image_candidates: Capacity[int]
    cutoffs: Table[SystemId, Array]

    @classmethod
    def new[S](
        cls,
        state: S,
        lens: Lens[S, IsCellListParams],
        cutoffs: Table[SystemId, Array],
    ) -> 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),
            cutoffs=cutoffs,
        )

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

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[Literal[2]]: ...
    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]: ...
    @jit
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        query_size = (
            queried_keys.size
            if queried_keys is not None
            else (queries.size if queries is not None else keys.size)
        )
        cutoffs = Table.broadcast_to(self.cutoffs, systems)
        pipeline = Pipeline[Literal[2]](
            selector=CellListSelector(
                cutoffs=cutoffs,
                max_cells=self.cells,
                max_candidates=self.avg_candidates.multiply(query_size),
                max_image_candidates=self.avg_image_candidates.multiply(query_size),
            ),
            masks=(
                InBoundsMask(),
                InclusionMatchMask(),
                QueriedKeysDedupMask(),
                DistanceCutoffMask(cutoffs=cutoffs),
                ExclusionMask(),
            ),
            compactor=ReduceCompactor(avg_edges=self.avg_edges.multiply(query_size)),
            postprocessors=(MirrorPairEdges(),),
        )
        if queries is not None:
            return pipeline(keys, systems, queries=queries)
        return pipeline(keys, systems, queried_keys=queried_keys)

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]]:
        query = ctx.query_table
        candidates = _cell_list_subselect(
            ctx.keys,
            query,
            ctx.systems,
            cutoffs=self.cutoffs.data,
            max_num_cells=self.max_cells,
            max_num_candidates=self.max_candidates,
        )
        candidates = lift_query_candidates(candidates, ctx)
        return replicate_for_images(
            candidates,
            ctx.keys,
            ctx.edge_query_table,
            ctx.systems,
            self.cutoffs,
            self.max_image_candidates,
        )

Compactor

Bases: Protocol

Produces compacted Edges[D] from the accumulated keep mask.

Source code in src/kups/core/neighborlist/types.py
class Compactor[D: int](Protocol):
    """Produces compacted ``Edges[D]`` from the accumulated ``keep`` mask."""

    def __call__(
        self, keep: Array, batch: CandidateBatch[D], ctx: PipelineContext
    ) -> Edges[D]: ...

DenseNearestNeighborList

Dense O(N²) neighbor list respecting system boundaries.

This implementation generates all particle pairs within each system separately, avoiding cross-system interactions. Efficient when the cutoff is comparable to the box size (cutoff/box ~ 1).

Complexity: O(N² / K²) where N is total particles and K is number of systems.

Attributes:

Name Type Description
avg_candidates Capacity[int]

Capacity for candidate pair storage.

avg_edges Capacity[int]

Capacity for final edge array.

avg_image_candidates Capacity[int]

Capacity for image candidate pairs.

cutoffs Table[SystemId, Array]

Per-system cutoff distances used by this neighbor list.

When to use
  • When cutoff/box_size ~ 1 (cutoff comparable to box dimensions)
  • Small box relative to cutoff (few cells would fit)
  • Non-periodic systems
Example
# Example: 15 Å cutoff in 20 Å box → cutoff/box = 0.75
nl = DenseNearestNeighborList.new(state, lens(lambda s: s.nl_params), cutoffs)

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

edges = nl(particles, systems)
Source code in src/kups/core/neighborlist/dense.py
@dataclass
class DenseNearestNeighborList:
    """Dense O(N²) neighbor list respecting system boundaries.

    This implementation generates all particle pairs within each system
    separately, avoiding cross-system interactions. Efficient when the cutoff
    is comparable to the box size (cutoff/box ~ 1).

    Complexity: O(N² / K²) where N is total particles and K is number of systems.

    Attributes:
        avg_candidates: Capacity for candidate pair storage.
        avg_edges: Capacity for final edge array.
        avg_image_candidates: Capacity for image candidate pairs.
        cutoffs: Per-system cutoff distances used by this neighbor list.

    When to use:
        - When cutoff/box_size ~ 1 (cutoff comparable to box dimensions)
        - Small box relative to cutoff (few cells would fit)
        - Non-periodic systems

    Example:
        ```python
        # Example: 15 Å cutoff in 20 Å box → cutoff/box = 0.75
        nl = DenseNearestNeighborList.new(state, lens(lambda s: s.nl_params), cutoffs)

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

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

    avg_candidates: Capacity[int]
    avg_edges: Capacity[int]
    avg_image_candidates: Capacity[int]
    cutoffs: Table[SystemId, Array]

    @classmethod
    def new[S](
        cls,
        state: S,
        lens: Lens[S, IsDenseNeighborlistParams],
        cutoffs: Table[SystemId, Array],
    ) -> DenseNearestNeighborList:
        params = lens.get(state)
        return DenseNearestNeighborList(
            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),
            ),
            cutoffs=cutoffs,
        )

    @classmethod
    def from_state(
        cls,
        state: IsNeighborListState[IsDenseNeighborlistParams],
        cutoffs: Table[SystemId, Array],
    ) -> DenseNearestNeighborList:
        return cls.new(state, lens(lambda s: s.neighborlist_params), cutoffs)

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[Literal[2]]: ...
    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]: ...
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        query_size = (
            queried_keys.size
            if queried_keys is not None
            else (queries.size if queries is not None else keys.size)
        )
        cutoffs = Table.broadcast_to(self.cutoffs, systems)
        pipeline = Pipeline[Literal[2]](
            selector=DenseSelector(
                cutoffs=cutoffs,
                max_candidates=self.avg_candidates.multiply(query_size),
                max_image_candidates=self.avg_image_candidates.multiply(query_size),
            ),
            masks=(
                InBoundsMask(),
                InclusionMatchMask(),
                QueriedKeysDedupMask(),
                DistanceCutoffMask(cutoffs=cutoffs),
                ExclusionMask(),
            ),
            compactor=ReduceCompactor(avg_edges=self.avg_edges.multiply(query_size)),
            postprocessors=(MirrorPairEdges(),),
        )
        if queries is not None:
            return pipeline(keys, systems, queries=queries)
        return pipeline(keys, systems, queried_keys=queried_keys)

DenseSelector

Selector for the per-system dense O(N²/K²) algorithm.

Source code in src/kups/core/neighborlist/dense.py
@dataclass
class DenseSelector:
    """Selector for the per-system dense ``O(N²/K²)`` algorithm."""

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

    def __call__(self, ctx: PipelineContext) -> CandidateBatch[Literal[2]]:
        query = ctx.query_table
        candidates = _dense_subselect(
            ctx.keys, query, ctx.systems, max_num_candidates=self.max_candidates
        )
        candidates = lift_query_candidates(candidates, ctx)
        return replicate_for_images(
            candidates,
            ctx.keys,
            ctx.edge_query_table,
            ctx.systems,
            self.cutoffs,
            self.max_image_candidates,
        )

DistanceCutoffMask

Drops candidates whose squared real-space distance exceeds cutoff².

Source code in src/kups/core/neighborlist/masks.py
@dataclass
class DistanceCutoffMask:
    """Drops candidates whose squared real-space distance exceeds ``cutoff²``."""

    cutoffs: Table[SystemId, Array]

    def __call__(
        self, batch: CandidateBatch[Literal[2]], ctx: PipelineContext
    ) -> Array:
        cutoffs = Table.broadcast_to(self.cutoffs, ctx.systems)
        shifts = batch.edges.shifts[:, 0, :]
        frame_table: Table[SystemId, MaterializedFrame] = ctx.systems.map_data(
            lambda s: s.cell.frame.materialize()
        )
        key_system = ctx.keys[batch.key_idx].system
        frames: MaterializedFrame = frame_table[key_system]
        if ctx.queries is None:
            pair_positions = ctx.keys[batch.edges.indices].positions
            key_positions = pair_positions[:, 0]
            query_positions = pair_positions[:, 1]
        else:
            key_positions = ctx.keys[batch.key_idx].positions
            query_positions = ctx.queries[batch.query_idx].positions
        dist_sq = real_distance_sq(key_positions, query_positions, frames, shifts)
        return dist_sq < cutoffs[key_system] ** 2

Edges

Bases: Sliceable

Represents edges (connections) between particles in a molecular system.

An edge connects Degree particles, where degree=2 represents pairwise interactions (bonds), degree=3 represents three-body interactions (angles), etc.

For periodic systems, edges include shift vectors that indicate how many cells to traverse when computing distances between connected particles.

Class Type Parameters:

Name Bound or Constraints Description Default
Degree int

Number of particles connected by each edge (static type check)

required

Attributes:

Name Type Description
indices Index[ParticleId]

Particle indices for each edge, shape (n_edges, Degree)

shifts Array

Periodic shift vectors, shape (n_edges, Degree-1, 3). Shift vectors for the 2nd through Degree-th particle relative to the first.

Example
# Pairwise edges (bonds) between particles
edges = Edges(
    indices=jnp.array([[0, 1], [1, 2], [0, 2]]),  # 3 edges
    shifts=jnp.array([[[0, 0, 0]], [[0, 0, 0]], [[1, 0, 0]]])  # 3rd edge crosses boundary
)
Source code in src/kups/core/neighborlist/edges.py
@dataclass
class Edges[Degree: int](Sliceable):
    """Represents edges (connections) between particles in a molecular system.

    An edge connects `Degree` particles, where degree=2 represents pairwise
    interactions (bonds), degree=3 represents three-body interactions (angles), etc.

    For periodic systems, edges include shift vectors that indicate how many
    cells to traverse when computing distances between connected particles.

    Type Parameters:
        Degree: Number of particles connected by each edge (static type check)

    Attributes:
        indices: Particle indices for each edge, shape `(n_edges, Degree)`
        shifts: Periodic shift vectors, shape `(n_edges, Degree-1, 3)`.
            Shift vectors for the 2nd through Degree-th particle relative to the first.

    Example:
        ```python
        # Pairwise edges (bonds) between particles
        edges = Edges(
            indices=jnp.array([[0, 1], [1, 2], [0, 2]]),  # 3 edges
            shifts=jnp.array([[[0, 0, 0]], [[0, 0, 0]], [[1, 0, 0]]])  # 3rd edge crosses boundary
        )
        ```
    """

    # The degree is purely for type checking and does not affect runtime behavior
    indices: Index[ParticleId]  # (n_edges, Degree)
    shifts: Array  # (n_edges, Degree - 1, 3)

    def __post_init__(self) -> None:
        # Resolve the underlying array for validation
        raw = self.indices.indices if isinstance(self.indices, Index) else self.indices
        if not isinstance(raw, Array):
            return
        assert jnp.issubdtype(raw.dtype, jnp.integer), (
            f"Indices must be of integer type, got {raw.dtype}"
        )
        target_shape = (
            *self.indices.shape[:-1],
            self.indices.shape[-1] - 1 if self.indices.shape[-1] > 1 else 0,
            3,
        )
        assert self.shifts.shape == target_shape, (
            f"Shifts must have shape {target_shape}, got {self.shifts.shape}"
        )

    def difference_vectors(
        self,
        particles: Table[ParticleId, HasPositionsAndSystemIndex],
        systems: Table[SystemId, HasCell[AnyPeriodicity]],
    ) -> Array:
        """Compute difference vectors between connected particles.

        For each edge, computes the vector from the first particle to each
        subsequent particle, accounting for periodic boundary conditions.

        Args:
            particles: Particle positions with system index information.
            systems: System data with cell for periodic boundary conditions.

        Returns:
            Array of shape `(n_edges, Degree-1, 3)` containing difference vectors.
        """

        shifts = self.absolute_shifts(particles, systems)
        pos = particles[self.indices].positions
        return pos[:, 1:] - pos[:, :1] + shifts

    def absolute_shifts(
        self,
        particles: Table[ParticleId, HasPositionsAndSystemIndex],
        systems: Table[SystemId, HasCell[AnyPeriodicity]],
    ) -> Array:
        """Compute absolute shift vectors for all particles in each edge.

        Converts relative shifts to absolute Cartesian shift vectors.

        Args:
            particles: Particle data with system index information.
            systems: System data with cell for periodic boundary conditions.

        Returns:
            Array of shape `(n_edges, Degree-1, 3)` containing absolute shift vectors.
        """
        lattice = systems.map_data(lambda x: x.cell.materialize())
        cells = lattice[particles[self.indices[:, 0]].system][:, None]
        return cells.frame.to_real(self.shifts)

    @property
    def degree(self) -> int:
        return self.indices.shape[-1]

    @override
    def __len__(self) -> int:
        return self.indices.shape[0]

absolute_shifts(particles, systems)

Compute absolute shift vectors for all particles in each edge.

Converts relative shifts to absolute Cartesian shift vectors.

Parameters:

Name Type Description Default
particles Table[ParticleId, HasPositionsAndSystemIndex]

Particle data with system index information.

required
systems Table[SystemId, HasCell[AnyPeriodicity]]

System data with cell for periodic boundary conditions.

required

Returns:

Type Description
Array

Array of shape (n_edges, Degree-1, 3) containing absolute shift vectors.

Source code in src/kups/core/neighborlist/edges.py
def absolute_shifts(
    self,
    particles: Table[ParticleId, HasPositionsAndSystemIndex],
    systems: Table[SystemId, HasCell[AnyPeriodicity]],
) -> Array:
    """Compute absolute shift vectors for all particles in each edge.

    Converts relative shifts to absolute Cartesian shift vectors.

    Args:
        particles: Particle data with system index information.
        systems: System data with cell for periodic boundary conditions.

    Returns:
        Array of shape `(n_edges, Degree-1, 3)` containing absolute shift vectors.
    """
    lattice = systems.map_data(lambda x: x.cell.materialize())
    cells = lattice[particles[self.indices[:, 0]].system][:, None]
    return cells.frame.to_real(self.shifts)

difference_vectors(particles, systems)

Compute difference vectors between connected particles.

For each edge, computes the vector from the first particle to each subsequent particle, accounting for periodic boundary conditions.

Parameters:

Name Type Description Default
particles Table[ParticleId, HasPositionsAndSystemIndex]

Particle positions with system index information.

required
systems Table[SystemId, HasCell[AnyPeriodicity]]

System data with cell for periodic boundary conditions.

required

Returns:

Type Description
Array

Array of shape (n_edges, Degree-1, 3) containing difference vectors.

Source code in src/kups/core/neighborlist/edges.py
def difference_vectors(
    self,
    particles: Table[ParticleId, HasPositionsAndSystemIndex],
    systems: Table[SystemId, HasCell[AnyPeriodicity]],
) -> Array:
    """Compute difference vectors between connected particles.

    For each edge, computes the vector from the first particle to each
    subsequent particle, accounting for periodic boundary conditions.

    Args:
        particles: Particle positions with system index information.
        systems: System data with cell for periodic boundary conditions.

    Returns:
        Array of shape `(n_edges, Degree-1, 3)` containing difference vectors.
    """

    shifts = self.absolute_shifts(particles, systems)
    pos = particles[self.indices].positions
    return pos[:, 1:] - pos[:, :1] + shifts

EmptyNeighborList

Neighbor list that emits an :class:Edges[D] with zero rows.

The degree field is the runtime arity carried by the emitted edges; it must match the type parameter D.

Attributes:

Name Type Description
degree int

Edge arity (Literal[0] for point clouds, higher for unified graph constructors that need a degree-aware empty NL).

Source code in src/kups/core/neighborlist/fixed.py
@dataclass
class EmptyNeighborList[D: int]:
    """Neighbor list that emits an :class:`Edges[D]` with zero rows.

    The ``degree`` field is the runtime arity carried by the emitted edges;
    it must match the type parameter ``D``.

    Attributes:
        degree: Edge arity (``Literal[0]`` for point clouds, higher for
            unified graph constructors that need a degree-aware empty NL).
    """

    degree: int = field(static=True, default=0)

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[D]: ...

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]: ...

    @jit
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]:
        assert queries is None or queried_keys is None, (
            "Neighbor-list calls cannot combine queries with queried_keys."
        )
        del queries, systems, queried_keys
        shift_inner = self.degree - 1 if self.degree > 1 else 0
        return Edges(
            indices=Index(keys.keys, jnp.zeros((0, self.degree), dtype=int)),
            shifts=jnp.zeros((0, shift_inner, 3), dtype=int),
        )

ExclusionMask

Drops minimum-image pairs that share an exclusion segment.

Non-minimum-image periodic copies of excluded pairs survive (allowed when batch.is_minimum_image is False for that copy).

Source code in src/kups/core/neighborlist/masks.py
@dataclass
class ExclusionMask:
    """Drops minimum-image pairs that share an exclusion segment.

    Non-minimum-image periodic copies of excluded pairs survive (allowed when
    ``batch.is_minimum_image`` is False for that copy).
    """

    def __call__(
        self, batch: CandidateBatch[Literal[2]], ctx: PipelineContext
    ) -> Array:
        if ctx.queries is None:
            edge_excl = ctx.keys[batch.edges.indices].exclusion.indices
            return (edge_excl[:, 0] != edge_excl[:, 1]) | ~batch.is_minimum_image

        key_excl, query_excl = Index.match(
            ctx.keys[batch.key_idx].exclusion, ctx.queries[batch.query_idx].exclusion
        )
        return (key_excl != query_excl) | ~batch.is_minimum_image

FixedEdgesNeighborList

Neighbor list for a fixed topology edge set.

Full self-graph calls return all fixed topology rows with shifts computed from the current particle positions. Affected self-graph calls pass keyword-only queried_keys after updated particle data has been written into keys; the neighbor list returns only fixed rows touched by those affected keys ids. queries is reserved for true bipartite neighbor-list implementations and is not a fixed-edge update mechanism.

Attributes:

Name Type Description
indices Index[ParticleId]

Fixed edge topology. Shifts are intentionally not stored; they are computed from the call's current particle positions.

avg_edges Capacity[int] | None

Update-only average affected-edge capacity per affected keys id. Full calls ignore this field and use the stored topology length; affected calls default to the full edge-buffer size when it is not provided.

Source code in src/kups/core/neighborlist/fixed.py
@dataclass
class FixedEdgesNeighborList[D: int]:
    """Neighbor list for a fixed topology edge set.

    Full self-graph calls return all fixed topology rows with shifts computed
    from the current particle positions. Affected self-graph calls pass
    keyword-only ``queried_keys`` after updated particle data has been written
    into ``keys``; the neighbor list returns only fixed rows touched by those
    affected ``keys`` ids. ``queries`` is reserved for true bipartite
    neighbor-list implementations and is not a fixed-edge update mechanism.

    Attributes:
        indices: Fixed edge topology. Shifts are intentionally not stored;
            they are computed from the call's current particle positions.
        avg_edges: Update-only average affected-edge capacity per affected
            ``keys`` id. Full calls ignore this field and use the stored
            topology length; affected calls default to the full edge-buffer
            size when it is not provided.
    """

    indices: Index[ParticleId]
    avg_edges: Capacity[int] | None = None

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[D]: ...

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]: ...

    @jit
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]:
        assert queries is None or queried_keys is None, (
            "Neighbor-list calls cannot combine queries with queried_keys."
        )
        assert queries is None, "FixedEdgesNeighborList only supports self-graph calls."
        if queried_keys is None:
            max_edges = FixedCapacity(len(self.indices))
        else:
            max_edges = (
                self.avg_edges.multiply(queried_keys.size)
                if self.avg_edges is not None
                else FixedCapacity(len(self.indices))
            )

        pipeline = Pipeline[D](
            selector=_FixedEdgesSelector(self.indices),
            masks=(TouchesQueriedKeysMask(),),
            compactor=ReduceCompactor(max_edges),
        )
        return pipeline(keys, systems, queried_keys=queried_keys)

InBoundsMask

Drops candidates whose key/query indices fall outside the valid inclusion-segment range.

Implements the per-side inclusion.indices < num_labels check used to guard scatter/gather lookups when the candidate buffer is padded.

Source code in src/kups/core/neighborlist/masks.py
@dataclass
class InBoundsMask:
    """Drops candidates whose key/query indices fall outside the valid inclusion-segment range.

    Implements the per-side ``inclusion.indices < num_labels`` check used to
    guard scatter/gather lookups when the candidate buffer is padded.
    """

    def __call__[D: int](self, batch: CandidateBatch[D], ctx: PipelineContext) -> Array:
        ngraphs = ctx.keys.data.inclusion.num_labels
        key_inclusions = ctx.keys.map_data(lambda d: d.inclusion.indices < ngraphs)
        if ctx.queries is None:
            idx = batch.edges.indices.indices_in(key_inclusions.keys)
            edge_in = key_inclusions.data.at[idx].get(mode="fill", fill_value=False)
            return edge_in.all(axis=-1)

        query_inclusions = ctx.queries.map_data(lambda d: d.inclusion.indices < ngraphs)
        key_idx = batch.key_idx.indices_in(key_inclusions.keys)
        query_idx = batch.query_idx.indices_in(query_inclusions.keys)
        key_in = key_inclusions.data.at[key_idx].get(mode="fill", fill_value=False)
        query_in = query_inclusions.data.at[query_idx].get(
            mode="fill", fill_value=False
        )
        return key_in & query_in

InclusionGroupSelector

Pairs every particle with every other in the same inclusion segment.

Ignores the cutoff entirely. Shifts are int-typed minimum-image fractional rounds — matches today's all_connected_neighborlist (which is Ewald-only and assumed fully periodic).

Source code in src/kups/core/neighborlist/all_connected.py
@dataclass
class InclusionGroupSelector:
    """Pairs every particle with every other in the same inclusion segment.

    Ignores the cutoff entirely. Shifts are int-typed minimum-image
    fractional rounds — matches today's ``all_connected_neighborlist``
    (which is Ewald-only and assumed fully periodic).
    """

    capacity: Capacity[int]

    def __call__(self, ctx: PipelineContext) -> CandidateBatch[Literal[2]]:
        query = ctx.query_table
        ngraphs = ctx.keys.data.inclusion.num_labels
        selection_result = subselect(
            ctx.keys.data.inclusion.indices,
            query.data.inclusion.indices,
            output_buffer_size=self.capacity,
            num_segments=ngraphs,
        )
        candidates = Candidates(
            key_idx=Index(ctx.keys.keys, selection_result.scatter_idxs),
            query_idx=Index(query.keys, selection_result.gather_idxs),
        )
        candidates = lift_query_candidates(candidates, ctx)
        query_tbl = ctx.edge_query_table
        deltas = (
            ctx.keys.data.positions[candidates.key_idx.indices]
            - query_tbl.data.positions[candidates.query_idx.indices]
        )
        shifts = jnp.round(deltas).astype(int)
        return candidates_to_batch(
            candidates,
            shifts,
            jnp.ones((candidates.key_idx.size,), dtype=bool),
        )

InclusionMatchMask

Drops candidates whose key/query inclusion segments differ.

Source code in src/kups/core/neighborlist/masks.py
@dataclass
class InclusionMatchMask:
    """Drops candidates whose key/query inclusion segments differ."""

    def __call__[D: int](self, batch: CandidateBatch[D], ctx: PipelineContext) -> Array:
        if ctx.queries is None:
            edge_incl = ctx.keys[batch.edges.indices].inclusion.indices
            return (edge_incl == edge_incl[:, :1]).all(axis=-1)

        key_incl, query_incl = Index.match(
            ctx.keys[batch.key_idx].inclusion, ctx.queries[batch.query_idx].inclusion
        )
        return key_incl == query_incl

IsAllDenseNeighborListParams

Bases: Protocol

Protocol for parameters required by AllDenseNearestNeighborList.

Source code in src/kups/core/neighborlist/all_dense.py
class IsAllDenseNeighborListParams(Protocol):
    """Protocol for parameters required by ``AllDenseNearestNeighborList``."""

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

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: ...

IsDenseNeighborlistParams

Bases: Protocol

Protocol for parameters required by DenseNearestNeighborList.

Source code in src/kups/core/neighborlist/dense.py
class IsDenseNeighborlistParams(Protocol):
    """Protocol for parameters required by ``DenseNearestNeighborList``."""

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

IsNeighborListState

Bases: Protocol

Protocol for states that expose neighbor list parameters.

A state satisfying this protocol can be passed to from_state() on any neighbor list class. The type parameter P determines which neighbor list types the state can construct (e.g., IsAllDenseNeighborListParams, IsDenseNeighborlistParams, IsCellListParams, or IsUniversalNeighborlistParams).

Source code in src/kups/core/neighborlist/types.py
class IsNeighborListState[P](Protocol):
    """Protocol for states that expose neighbor list parameters.

    A state satisfying this protocol can be passed to ``from_state()`` on any
    neighbor list class. The type parameter ``P`` determines which neighbor
    list types the state can construct (e.g., ``IsAllDenseNeighborListParams``,
    ``IsDenseNeighborlistParams``, ``IsCellListParams``, or
    ``IsUniversalNeighborlistParams``).
    """

    @property
    def neighborlist_params(self) -> P: ...

IsUniversalNeighborlistParams

Bases: Protocol

Protocol for parameters required by any neighbor list implementation.

A superset of IsAllDenseNeighborListParams, IsDenseNeighborlistParams, and IsCellListParams. Satisfying this protocol allows constructing any of the three neighbor list types.

Source code in src/kups/core/neighborlist/types.py
class IsUniversalNeighborlistParams(Protocol):
    """Protocol for parameters required by any neighbor list implementation.

    A superset of ``IsAllDenseNeighborListParams``, ``IsDenseNeighborlistParams``,
    and ``IsCellListParams``. Satisfying this protocol allows constructing any
    of the three neighbor list types.
    """

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

Mask

Bases: Protocol

Returns this criterion's bool array; pipeline conjuncts the results.

The degree parameter tracks which candidate arity the mask accepts. Pair masks implement Mask[Literal[2]] by annotating their batch argument as CandidateBatch[Literal[2]]; degree-agnostic masks use a generic __call__ method.

Cannot change batch.edges, batch.is_minimum_image, or the candidate count. Pure (batch, ctx) -> Array.

Source code in src/kups/core/neighborlist/types.py
class Mask[D: int](Protocol):
    """Returns this criterion's bool array; pipeline conjuncts the results.

    The degree parameter tracks which candidate arity the mask accepts. Pair
    masks implement ``Mask[Literal[2]]`` by annotating their ``batch`` argument
    as ``CandidateBatch[Literal[2]]``; degree-agnostic masks use a generic
    ``__call__`` method.

    Cannot change ``batch.edges``, ``batch.is_minimum_image``, or the
    candidate count. Pure ``(batch, ctx) -> Array``.
    """

    def __call__(self, batch: CandidateBatch[D], ctx: PipelineContext) -> Array: ...

MaskOnlyCompactor

Bases: Compactor[D]

In-place compaction: failing entries become OOB indices and zero shifts.

No size change; preserves the candidate count from the selector. Pair candidates are already in their output index space.

Source code in src/kups/core/neighborlist/compact.py
@dataclass
class MaskOnlyCompactor[D: int](Compactor[D]):
    """In-place compaction: failing entries become OOB indices and zero shifts.

    No size change; preserves the candidate count from the selector. Pair
    candidates are already in their output index space.
    """

    def __call__(
        self,
        keep: Array,
        batch: CandidateBatch[D],
        ctx: PipelineContext,
    ) -> Edges[D]:
        oob = max(ctx.keys.size, ctx.edge_query_table.size)
        indices_in = batch.edges.indices.indices
        indices = where_broadcast_last(keep, indices_in, oob)
        shifts = where_broadcast_last(keep, batch.edges.shifts, 0)
        return Edges(Index(batch.edges.indices.keys, indices), shifts)

MirrorPairEdges

Bases: Postprocessor[Literal[2]]

Append reversed pair edges for undirected graph outputs.

The default mirrors only self-graph update calls selected by ctx.queried_keys. Full self-neighbor calls already emit both directions before compaction, while queried_keys calls operate on affected ids in the already-updated keys table and are deduplicated by QueriedKeysDedupMask. Their reverse edges are restored after compaction.

Attributes:

Name Type Description
only_when_queried_keys bool

When True, no-op unless ctx.queried_keys is active; ctx.queries is not involved. Set to False for pipelines whose selector emits only one direction even in full calls.

Source code in src/kups/core/neighborlist/postprocess.py
@dataclass
class MirrorPairEdges(Postprocessor[Literal[2]]):
    """Append reversed pair edges for undirected graph outputs.

    The default mirrors only self-graph update calls selected by
    ``ctx.queried_keys``. Full self-neighbor calls already emit both directions
    before compaction, while ``queried_keys`` calls operate on affected ids in
    the already-updated ``keys`` table and are deduplicated by
    ``QueriedKeysDedupMask``. Their reverse edges are restored after compaction.

    Attributes:
        only_when_queried_keys: When ``True``, no-op unless ``ctx.queried_keys``
            is active; ``ctx.queries`` is not involved. Set to ``False`` for
            pipelines whose selector emits only one direction even in full
            calls.
    """

    only_when_queried_keys: bool = field(default=True, static=True)

    def __call__(
        self, edges: Edges[Literal[2]], ctx: PipelineContext
    ) -> Edges[Literal[2]]:
        if self.only_when_queried_keys and ctx.queried_keys is None:
            return edges

        indices = edges.indices.indices
        mirrored_indices = jnp.concatenate([indices, indices[:, ::-1]], axis=0)
        mirrored_shifts = jnp.concatenate([edges.shifts, -edges.shifts], axis=0)
        return Edges(Index(edges.indices.keys, mirrored_indices), mirrored_shifts)

NeighborList

Bases: Protocol

Protocol for neighbor list construction algorithms.

Implementations find groups of particles within a cutoff distance, handling periodic boundary conditions and inclusion/exclusion masks. The degree parameter tracks the arity of the emitted edge tuples.

Source code in src/kups/core/neighborlist/types.py
class NeighborList[D: int](Protocol):
    """Protocol for neighbor list construction algorithms.

    Implementations find groups of particles within a cutoff distance, handling
    periodic boundary conditions and inclusion/exclusion masks. The degree
    parameter tracks the arity of the emitted edge tuples.
    """

    @overload
    def __call__[P: NeighborListPoints](
        self,
        keys: Table[ParticleId, P],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, P],
    ) -> Edges[D]: ...
    @overload
    def __call__[P: NeighborListPoints](
        self,
        keys: Table[ParticleId, P],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]: ...
    def __call__[P: NeighborListPoints](
        self,
        keys: Table[ParticleId, P],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, P] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]:
        """Find particle groups for a self-graph or bipartite query.

        ``queries`` and ``queried_keys`` are mutually exclusive; the overloads
        above reject calls that pass both.

        Args:
            keys: Particle table that the returned ``Edges`` index into. With
                neither ``queries`` nor ``queried_keys``, the neighbor list
                builds a self-graph over ``keys``.
            systems: Indexed system data with cell information.
            queries: Optional bipartite query table. Each edge then connects a
                ``keys`` particle to a ``queries`` particle.
            queried_keys: Optional subset of ``keys`` particle ids whose
                incident self-graph edges should be returned. The caller must
                already have written any updated particle data into ``keys``.

        Returns:
            Edges whose columns index ``keys`` for self-graph calls.
        """
        ...

__call__(keys, systems, *, queries=None, queried_keys=None)

__call__(
    keys: Table[ParticleId, P],
    systems: Table[SystemId, NeighborListSystems],
    *,
    queries: Table[ParticleId, P],
) -> Edges[D]
__call__(
    keys: Table[ParticleId, P],
    systems: Table[SystemId, NeighborListSystems],
    *,
    queried_keys: Index[ParticleId] | None = None,
) -> Edges[D]

Find particle groups for a self-graph or bipartite query.

queries and queried_keys are mutually exclusive; the overloads above reject calls that pass both.

Parameters:

Name Type Description Default
keys Table[ParticleId, P]

Particle table that the returned Edges index into. With neither queries nor queried_keys, the neighbor list builds a self-graph over keys.

required
systems Table[SystemId, NeighborListSystems]

Indexed system data with cell information.

required
queries Table[ParticleId, P] | None

Optional bipartite query table. Each edge then connects a keys particle to a queries particle.

None
queried_keys Index[ParticleId] | None

Optional subset of keys particle ids whose incident self-graph edges should be returned. The caller must already have written any updated particle data into keys.

None

Returns:

Type Description
Edges[D]

Edges whose columns index keys for self-graph calls.

Source code in src/kups/core/neighborlist/types.py
def __call__[P: NeighborListPoints](
    self,
    keys: Table[ParticleId, P],
    systems: Table[SystemId, NeighborListSystems],
    *,
    queries: Table[ParticleId, P] | None = None,
    queried_keys: Index[ParticleId] | None = None,
) -> Edges[D]:
    """Find particle groups for a self-graph or bipartite query.

    ``queries`` and ``queried_keys`` are mutually exclusive; the overloads
    above reject calls that pass both.

    Args:
        keys: Particle table that the returned ``Edges`` index into. With
            neither ``queries`` nor ``queried_keys``, the neighbor list
            builds a self-graph over ``keys``.
        systems: Indexed system data with cell information.
        queries: Optional bipartite query table. Each edge then connects a
            ``keys`` particle to a ``queries`` particle.
        queried_keys: Optional subset of ``keys`` particle ids whose
            incident self-graph edges should be returned. The caller must
            already have written any updated particle data into ``keys``.

    Returns:
        Edges whose columns index ``keys`` for self-graph calls.
    """
    ...

NeighborListCandidate

A backing neighbor list paired with its cost estimator.

Attributes:

Name Type Description
neighborlist NeighborList[Literal[2]]

The candidate implementation.

cost NeighborListCost

Estimates the implementation's cost for a call's counts.

Source code in src/kups/core/neighborlist/adaptive.py
@dataclass
class NeighborListCandidate:
    """A backing neighbor list paired with its cost estimator.

    Attributes:
        neighborlist: The candidate implementation.
        cost: Estimates the implementation's cost for a call's counts.
    """

    neighborlist: NeighborList[Literal[2]]
    cost: NeighborListCost = field(static=True)

NeighborListCost

Bases: Protocol

Estimates the relative runtime cost of a neighbor list for one call.

Lower is cheaper; :class:AdaptiveNeighborList dispatches to the minimum. Return math.inf to mark an implementation invalid for the given shape.

Source code in src/kups/core/neighborlist/adaptive.py
class NeighborListCost(Protocol):
    """Estimates the relative runtime cost of a neighbor list for one call.

    Lower is cheaper; :class:`AdaptiveNeighborList` dispatches to the minimum.
    Return ``math.inf`` to mark an implementation invalid for the given shape.
    """

    def __call__(self, num_particles: int, num_systems: int) -> float: ...

NeighborListFactory

Bases: Protocol

Constructs a pair :class:NeighborList for a given state and cutoffs.

Used by radius-based potential factories so the construction strategy can be swapped without coupling the potential to a concrete neighbor-list class. The library default is :meth:kups.core.neighborlist.AdaptiveNeighborList.from_state, which is contravariant-compatible with any state satisfying :class:IsNeighborListState.

The State type parameter is contravariant (it appears only in input position in __call__), so a factory written against a broader state protocol can be passed where a narrower one is expected.

Source code in src/kups/core/neighborlist/types.py
class NeighborListFactory[State](Protocol):
    """Constructs a pair :class:`NeighborList` for a given state and cutoffs.

    Used by radius-based potential factories so the construction strategy
    can be swapped without coupling the potential to a concrete
    neighbor-list class. The library default is
    :meth:`kups.core.neighborlist.AdaptiveNeighborList.from_state`,
    which is contravariant-compatible with any state satisfying
    :class:`IsNeighborListState`.

    The ``State`` type parameter is contravariant (it appears only in
    input position in ``__call__``), so a factory written against a
    broader state protocol can be passed where a narrower one is expected.
    """

    def __call__(
        self,
        state: State,
        cutoffs: Table[SystemId, Array],
    ) -> NeighborList[Literal[2]]: ...

Pipeline

Selector → mask sequence → compactor → postprocessors.

Attributes:

Name Type Description
selector CandidateSelector[D]

Produces a CandidateBatch[D] (handles PBC replication).

masks tuple[Mask[D], ...]

Tuple of mask criteria over CandidateBatch[D]; results are conjuncted via &.

compactor Compactor[D]

Produces compacted Edges[D] from the accumulated mask.

postprocessors tuple[Postprocessor[D], ...]

Edge transforms applied sequentially after compaction.

Source code in src/kups/core/neighborlist/pipeline.py
@dataclass
class Pipeline[D: int]:
    """Selector → mask sequence → compactor → postprocessors.

    Attributes:
        selector: Produces a ``CandidateBatch[D]`` (handles PBC replication).
        masks: Tuple of mask criteria over ``CandidateBatch[D]``; results
            are conjuncted via ``&``.
        compactor: Produces compacted ``Edges[D]`` from the accumulated mask.
        postprocessors: Edge transforms applied sequentially after compaction.
    """

    selector: CandidateSelector[D]
    masks: tuple[Mask[D], ...] = field(static=True)
    compactor: Compactor[D]
    postprocessors: tuple[Postprocessor[D], ...] = field(default=(), static=True)

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[D]: ...
    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]: ...
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[D]:
        ctx = _prepare(keys, queries, systems, queried_keys)
        batch = self.selector(ctx)
        keep = jnp.ones((len(batch.edges),), dtype=bool)
        for mask in self.masks:
            keep &= mask(batch, ctx)
        edges = self.compactor(keep, batch, ctx)
        for postprocessor in self.postprocessors:
            edges = postprocessor(edges, ctx)
        return edges

PipelineContext

Read-only inputs shared by every mask and the compactor.

Positions in keys and optional queries are in fractional coordinates (transformed by [_prepare][kups.core.neighborlist.pipeline._prepare]). There is no out_of_bounds field — masks/compactors that need an OOB sentinel resolve the query table first.

Attributes:

Name Type Description
keys Table[ParticleId, NeighborListPoints]

Key particle table in fractional coords (the table the returned Edges index into).

queries Table[ParticleId, NeighborListPoints] | None

Query particle table in fractional coords for true bipartite queries. None for full self-graphs and queried_keys updates.

systems Table[SystemId, NeighborListSystems]

Indexed system data with cell information.

queried_keys Array | None

Raw keys positions to update/query, or None for a full self-graph or bipartite query.

Source code in src/kups/core/neighborlist/types.py
@dataclass
class PipelineContext:
    """Read-only inputs shared by every mask and the compactor.

    Positions in ``keys`` and optional ``queries`` are in **fractional**
    coordinates (transformed by
    [`_prepare`][kups.core.neighborlist.pipeline._prepare]). There is no
    ``out_of_bounds`` field — masks/compactors that need an OOB sentinel resolve
    the query table first.

    Attributes:
        keys: Key particle table in fractional coords (the table the returned
            ``Edges`` index into).
        queries: Query particle table in fractional coords for true bipartite
            queries. ``None`` for full self-graphs and ``queried_keys`` updates.
        systems: Indexed system data with cell information.
        queried_keys: Raw ``keys`` positions to update/query, or ``None`` for a
            full self-graph or bipartite query.
    """

    keys: Table[ParticleId, NeighborListPoints]
    queries: Table[ParticleId, NeighborListPoints] | None
    systems: Table[SystemId, NeighborListSystems]
    queried_keys: Array | None

    @skip_post_init_if_disabled
    def __post_init__(self) -> None:
        assert self.queries is None or self.queried_keys is None, (
            "PipelineContext cannot combine queries with queried_keys."
        )

    @property
    def edge_query_table(self) -> Table[ParticleId, NeighborListPoints]:
        """Table addressed by the second edge column."""
        return self.queries if self.queries is not None else self.keys

    @property
    def query_table(self) -> Table[ParticleId, NeighborListPoints]:
        """Table used to enumerate query candidates.

        ``queries`` is reserved for true bipartite calls. ``queried_keys``
        selects a self-graph update subset from ``keys`` and is lifted back to
        ``keys`` index space before masks and compaction see the batch.
        """
        if self.queries is not None:
            return self.queries
        if self.queried_keys is None:
            return self.keys
        return self.keys.subset(Index(self.keys.keys, self.queried_keys))

edge_query_table property

Table addressed by the second edge column.

query_table property

Table used to enumerate query candidates.

queries is reserved for true bipartite calls. queried_keys selects a self-graph update subset from keys and is lifted back to keys index space before masks and compaction see the batch.

Postprocessor

Bases: Protocol

Transforms compacted edges using the pipeline context.

Postprocessors run sequentially after compaction. They may change the number of rows, but must preserve the edge degree D.

Source code in src/kups/core/neighborlist/types.py
class Postprocessor[D: int](Protocol):
    """Transforms compacted edges using the pipeline context.

    Postprocessors run sequentially after compaction. They may change the
    number of rows, but must preserve the edge degree ``D``.
    """

    def __call__(self, edges: Edges[D], ctx: PipelineContext) -> Edges[D]: ...

PrecomputedEdgesSelector

Selector that wraps precomputed Edges for both refine variants.

Precomputed self-graph edges are already in keys space. A disjoint bipartite queries call may still use query positions for the second column, matching the edge convention of the original candidate set.

Attributes:

Name Type Description
candidates Edges[Literal[2]]

Precomputed edges (indices in keys-space).

recompute_mic_shifts bool

When True, drop the precomputed shifts and recompute minimum-image shifts on the current positions (RefineCutoffNeighborList — the precomputed shifts may be stale relative to the current cell). When False, reuse candidates.shifts directly (RefineMaskNeighborList). is_minimum_image is always all-True (no image replication).

Source code in src/kups/core/neighborlist/refine.py
@dataclass
class PrecomputedEdgesSelector:
    """Selector that wraps precomputed ``Edges`` for both refine variants.

    Precomputed self-graph edges are already in ``keys`` space. A disjoint
    bipartite ``queries`` call may still use query positions for the second
    column, matching the edge convention of the original candidate set.

    Attributes:
        candidates: Precomputed edges (indices in keys-space).
        recompute_mic_shifts: When ``True``, drop the precomputed shifts and
            recompute minimum-image shifts on the current positions
            (``RefineCutoffNeighborList`` — the precomputed shifts may be
            stale relative to the current cell). When ``False``, reuse
            ``candidates.shifts`` directly (``RefineMaskNeighborList``).
            ``is_minimum_image`` is always all-True (no image replication).
    """

    candidates: Edges[Literal[2]]
    recompute_mic_shifts: bool = field(static=True, default=False)

    def __call__(self, ctx: PipelineContext) -> CandidateBatch[Literal[2]]:
        if self.recompute_mic_shifts:
            indices = self.candidates.indices.indices
            query = ctx.edge_query_table
            raw_candidates = Candidates(
                key_idx=Index(ctx.keys.keys, indices[:, 0]),
                query_idx=Index(query.keys, indices[:, 1]),
            )
            return make_batch_with_mic(raw_candidates, ctx.keys, query, ctx.systems)
        indices = self.candidates.indices.indices
        edges = Edges(Index(ctx.keys.keys, indices), self.candidates.shifts)
        return CandidateBatch(
            edges=edges,
            is_minimum_image=jnp.ones((len(self.candidates),), dtype=bool),
            query_keys=ctx.edge_query_table.keys,
        )

QueriedKeysDedupMask

Deduplicate self-graph update candidates.

Pair selectors emit candidates in keys space. When ctx.queried_keys is set, the query side was restricted to those affected keys rows. We keep edges whose key endpoint is unaffected, plus one orientation for edges where both endpoints are affected. MirrorPairEdges restores the reverse orientation after compaction.

Returns all-True for full self-graphs and bipartite queries.

Source code in src/kups/core/neighborlist/masks.py
@dataclass
class QueriedKeysDedupMask:
    """Deduplicate self-graph update candidates.

    Pair selectors emit candidates in ``keys`` space. When ``ctx.queried_keys``
    is set, the query side was restricted to those affected ``keys`` rows.
    We keep edges whose key endpoint is unaffected, plus one orientation for
    edges where both endpoints are affected. ``MirrorPairEdges`` restores the
    reverse orientation after compaction.

    Returns all-True for full self-graphs and bipartite queries.
    """

    def __call__(
        self, batch: CandidateBatch[Literal[2]], ctx: PipelineContext
    ) -> Array:
        if ctx.queried_keys is None:
            return jnp.ones((batch.key_idx.size,), dtype=bool)
        return ~isin(batch.key_idx.indices, ctx.queried_keys, ctx.keys.size) | (
            batch.key_idx.indices >= batch.query_idx.indices
        )

ReduceCompactor

Bases: Compactor[D]

Compact surviving candidates to a size-bounded Edges[D].

Compacts whole candidate rows, so pair neighbor lists and fixed higher-degree topology share the same implementation.

Source code in src/kups/core/neighborlist/compact.py
@dataclass
class ReduceCompactor[D: int](Compactor[D]):
    """Compact surviving candidates to a size-bounded ``Edges[D]``.

    Compacts whole candidate rows, so pair neighbor lists and fixed higher-degree
    topology share the same implementation.
    """

    avg_edges: Capacity[int]

    def __call__(
        self,
        keep: Array,
        batch: CandidateBatch[D],
        ctx: PipelineContext,
    ) -> Edges[D]:
        oob = max(ctx.keys.size, ctx.edge_query_table.size)
        max_edges = self.avg_edges.generate_assertion(keep.sum())
        sort_idxs = jnp.where(keep, size=max_edges.size, fill_value=keep.size)[0]
        shifts = batch.edges.shifts.at[sort_idxs].get(
            mode="fill", fill_value=0, indices_are_sorted=True
        )

        indices = batch.edges.indices.indices.at[sort_idxs].get(
            mode="fill", fill_value=oob, indices_are_sorted=True
        )
        return Edges(Index(batch.edges.indices.keys, indices), shifts)

RefineCutoffNeighborList

Refine precomputed edges by re-checking distances with new cutoffs.

This neighbor list takes an existing set of candidate edges and filters them by computing actual distances and comparing to cutoffs. Enables sharing a single conservative neighbor list across multiple potentials with different cutoff distances.

Key benefit: Compute expensive neighbor list once with maximum cutoff, then refine for each potential with its specific cutoff (e.g., Lennard-Jones at 10 Å, Coulomb at 15 Å).

Attributes:

Name Type Description
candidates Edges[Literal[2]]

Precomputed edges to refine (should be conservative/over-inclusive).

avg_edges Capacity[int]

Capacity for output edge array.

cutoffs Table[SystemId, Array]

Per-system cutoff distances used by this refinement.

Use cases
  • Multiple potentials sharing one neighbor list with different cutoffs
  • Multi-stage neighbor list construction (coarse then fine)
  • Adaptive cutoffs that change during simulation
  • Using a static "super" neighbor list with varying actual cutoffs
Example
# Compute base neighbor list once with maximum cutoff
max_cutoff = 15.0  # Maximum of all potential cutoffs
base_edges = base_nl(particles, cells)

# Share across potentials with different cutoffs
lj_nl = RefineCutoffNeighborList(
    candidates=base_edges, avg_edges=cap1, cutoffs=lj_cutoffs
)
lj_edges = lj_nl(particles, cells)  # LJ cutoff

coulomb_nl = RefineCutoffNeighborList(
    candidates=base_edges, avg_edges=cap2, cutoffs=coulomb_cutoffs
)
coulomb_edges = coulomb_nl(particles, cells)  # Coulomb cutoff
Source code in src/kups/core/neighborlist/refine.py
@dataclass
class RefineCutoffNeighborList:
    """Refine precomputed edges by re-checking distances with new cutoffs.

    This neighbor list takes an existing set of candidate edges and filters them
    by computing actual distances and comparing to cutoffs. Enables sharing a
    single conservative neighbor list across multiple potentials with different
    cutoff distances.

    **Key benefit**: Compute expensive neighbor list once with maximum cutoff,
    then refine for each potential with its specific cutoff (e.g., Lennard-Jones
    at 10 Å, Coulomb at 15 Å).

    Attributes:
        candidates: Precomputed edges to refine (should be conservative/over-inclusive).
        avg_edges: Capacity for output edge array.
        cutoffs: Per-system cutoff distances used by this refinement.

    Use cases:
        - Multiple potentials sharing one neighbor list with different cutoffs
        - Multi-stage neighbor list construction (coarse then fine)
        - Adaptive cutoffs that change during simulation
        - Using a static "super" neighbor list with varying actual cutoffs

    Example:
        ```python
        # Compute base neighbor list once with maximum cutoff
        max_cutoff = 15.0  # Maximum of all potential cutoffs
        base_edges = base_nl(particles, cells)

        # Share across potentials with different cutoffs
        lj_nl = RefineCutoffNeighborList(
            candidates=base_edges, avg_edges=cap1, cutoffs=lj_cutoffs
        )
        lj_edges = lj_nl(particles, cells)  # LJ cutoff

        coulomb_nl = RefineCutoffNeighborList(
            candidates=base_edges, avg_edges=cap2, cutoffs=coulomb_cutoffs
        )
        coulomb_edges = coulomb_nl(particles, cells)  # Coulomb cutoff
        ```
    """

    candidates: Edges[Literal[2]]
    avg_edges: Capacity[int]
    cutoffs: Table[SystemId, Array]

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[Literal[2]]: ...

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]: ...

    @jit
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        resolved_keys, resolved_queries = _resolve_precomputed_inputs(
            keys, queries, queried_keys
        )
        query_size = (
            queried_keys.size
            if queried_keys is not None
            else (queries.size if queries is not None else keys.size)
        )
        cutoffs = Table.broadcast_to(self.cutoffs, systems)
        pipeline = Pipeline[Literal[2]](
            selector=PrecomputedEdgesSelector(
                self.candidates, recompute_mic_shifts=True
            ),
            masks=(
                InBoundsMask(),
                InclusionMatchMask(),
                DistanceCutoffMask(cutoffs=cutoffs),
                TouchesQueriedKeysMask(),
                ExclusionMask(),
            ),
            compactor=ReduceCompactor(avg_edges=self.avg_edges.multiply(query_size)),
        )
        if resolved_queries is not None:
            return pipeline(resolved_keys, systems, queries=resolved_queries)
        return pipeline(resolved_keys, systems, queried_keys=queried_keys)

RefineMaskNeighborList

Refine a precomputed neighbor list by applying inclusion/exclusion masks.

This neighbor list takes an existing set of candidate edges and filters them based on segmentation masks, without recomputing distances. Enables sharing a single base neighbor list across multiple potentials with different interaction rules.

Key benefit: Compute expensive neighbor list once, apply different masks for different potentials (e.g., Lennard-Jones excludes 1-4 interactions, Coulomb has different exclusions).

Attributes:

Name Type Description
candidates Edges[Literal[2]]

Precomputed edges to refine

Use cases
  • Multiple potentials sharing one neighbor list with different exclusions
  • Excluding bonded pairs (1-2, 1-3, 1-4) from non-bonded interactions
  • Applying group-specific interaction rules
  • Multi-scale simulations with different interaction levels
Example
# Compute base neighbor list once
base_edges = base_nl(particles, cells)

# Share across potentials with different masks
lj_nl = RefineMaskNeighborList(candidates=base_edges)
lj_edges = lj_nl(lj_particles, cells)  # 1-4 exclusions

coulomb_nl = RefineMaskNeighborList(candidates=base_edges)
coulomb_edges = coulomb_nl(coulomb_particles, cells)  # 1-2 exclusions only
Source code in src/kups/core/neighborlist/refine.py
@dataclass
class RefineMaskNeighborList:
    """Refine a precomputed neighbor list by applying inclusion/exclusion masks.

    This neighbor list takes an existing set of candidate edges and filters them
    based on segmentation masks, without recomputing distances. Enables sharing
    a single base neighbor list across multiple potentials with different
    interaction rules.

    **Key benefit**: Compute expensive neighbor list once, apply different masks
    for different potentials (e.g., Lennard-Jones excludes 1-4 interactions,
    Coulomb has different exclusions).

    Attributes:
        candidates: Precomputed edges to refine

    Use cases:
        - Multiple potentials sharing one neighbor list with different exclusions
        - Excluding bonded pairs (1-2, 1-3, 1-4) from non-bonded interactions
        - Applying group-specific interaction rules
        - Multi-scale simulations with different interaction levels

    Example:
        ```python
        # Compute base neighbor list once
        base_edges = base_nl(particles, cells)

        # Share across potentials with different masks
        lj_nl = RefineMaskNeighborList(candidates=base_edges)
        lj_edges = lj_nl(lj_particles, cells)  # 1-4 exclusions

        coulomb_nl = RefineMaskNeighborList(candidates=base_edges)
        coulomb_edges = coulomb_nl(coulomb_particles, cells)  # 1-2 exclusions only
        ```
    """

    candidates: Edges[Literal[2]]

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints],
    ) -> Edges[Literal[2]]: ...

    @overload
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]: ...

    @jit
    def __call__(
        self,
        keys: Table[ParticleId, NeighborListPoints],
        systems: Table[SystemId, NeighborListSystems],
        *,
        queries: Table[ParticleId, NeighborListPoints] | None = None,
        queried_keys: Index[ParticleId] | None = None,
    ) -> Edges[Literal[2]]:
        resolved_keys, resolved_queries = _resolve_precomputed_inputs(
            keys, queries, queried_keys
        )
        pipeline = Pipeline[Literal[2]](
            selector=PrecomputedEdgesSelector(self.candidates),
            masks=(
                InBoundsMask(),
                InclusionMatchMask(),
                TouchesQueriedKeysMask(),
                ExclusionMask(),
            ),
            compactor=MaskOnlyCompactor(),
        )
        if resolved_queries is not None:
            return pipeline(resolved_keys, systems, queries=resolved_queries)
        return pipeline(resolved_keys, systems, queried_keys=queried_keys)

TouchesQueriedKeysMask

Keep fixed-topology rows touched by ctx.queried_keys.

When no affected-index subset is active, every row is kept. This lets fixed topology use the same pipeline for full and patch-shaped calls.

Source code in src/kups/core/neighborlist/masks.py
@dataclass
class TouchesQueriedKeysMask[D: int]:
    """Keep fixed-topology rows touched by ``ctx.queried_keys``.

    When no affected-index subset is active, every row is kept. This lets fixed
    topology use the same pipeline for full and patch-shaped calls.
    """

    def __call__(self, batch: CandidateBatch[D], ctx: PipelineContext) -> Array:
        if ctx.queried_keys is None:
            return jnp.ones((len(batch.edges),), dtype=bool)
        return isin(batch.edges.indices.indices, ctx.queried_keys, ctx.keys.size).any(
            -1
        )

UniversalNeighborlistParameters

Concrete parameter dataclass satisfying IsUniversalNeighborlistParams.

Holds the capacity hints needed by every neighbor list implementation. Use the estimate() classmethod to compute reasonable initial values from system geometry rather than guessing manually.

Attributes:

Name Type Description
avg_edges int

Average number of edges per particle (for edge capacity).

avg_candidates int

Average number of candidate pairs per particle.

avg_image_candidates int

Average number of candidate pairs per particle after periodic-image replication (equals avg_candidates when every cutoff stays within the minimum-image regime).

cells int

Maximum number of spatial hash cells across all systems.

Source code in src/kups/core/neighborlist/parameters.py
@dataclass
class UniversalNeighborlistParameters:
    """Concrete parameter dataclass satisfying ``IsUniversalNeighborlistParams``.

    Holds the capacity hints needed by every neighbor list implementation.
    Use the ``estimate()`` classmethod to compute reasonable initial values
    from system geometry rather than guessing manually.

    Attributes:
        avg_edges: Average number of edges per particle (for edge capacity).
        avg_candidates: Average number of candidate pairs per particle.
        avg_image_candidates: Average number of candidate pairs per particle after
            periodic-image replication (equals ``avg_candidates`` when every cutoff
            stays within the minimum-image regime).
        cells: Maximum number of spatial hash cells across all systems.
    """

    avg_edges: int = field(static=True)
    avg_candidates: int = field(static=True)
    avg_image_candidates: int = field(static=True)
    cells: int = field(static=True)

    @classmethod
    @no_jax_tracing
    def estimate(
        cls,
        particles_per_system: Table[SystemId, Array],
        systems: Table[SystemId, NeighborListSystems],
        cutoffs: Table[SystemId, Array],
        *,
        base: float = 2,
        multiplier: float = 1.0,
    ) -> UniversalNeighborlistParameters:
        """Estimate parameters for all neighbor list types from system geometry.

        Computes conservative initial capacities based on particle density
        and cutoff radii. The estimates are rounded up to the next power of
        ``base`` to amortize future resizing.

        Args:
            particles_per_system: Number of particles per system.
            systems: System data with cell information.
            cutoffs: Cutoff distance per system.
            base: Base for power-of rounding (default 2).
            multiplier: Safety factor applied to the estimate (default 1.0).

        Returns:
            A ``UniversalNeighborlistParameters`` instance with estimated values.
        """

        def _next_power(total: float | Array) -> int:
            return int(next_higher_power(jnp.array(total * multiplier), base=base))

        sys = Table.join(systems, particles_per_system, cutoffs)
        total_candidates = total_image_candidates = total_edges = max_cells = 0
        with no_post_init():
            for _, (s, n_p, c) in sys:
                n_bins = num_cells(s, c).prod()
                candidates = min(n_p / n_bins * (3**3), n_p)
                # A cutoff reaching past perp/2 replicates each candidate once per
                # periodic image (product of per-axis image counts). Summing per
                # system keeps the estimate tight for heterogeneous cutoffs instead
                # of assuming every system replicates at the maximum rate.
                images = candidate_image_counts(s.cell, c).prod()
                total_candidates += candidates
                total_image_candidates += _next_power(candidates) * images
                total_edges += _estimate_avg_num_edges(
                    n_p, s.cell.volume, c, base, multiplier
                )
                max_cells = max(n_bins, max_cells)

        return UniversalNeighborlistParameters(
            avg_edges=int(total_edges // sys.size),
            avg_candidates=_next_power(total_candidates / sys.size),
            avg_image_candidates=_next_power(total_image_candidates / sys.size),
            cells=int(max_cells),
        )

estimate(particles_per_system, systems, cutoffs, *, base=2, multiplier=1.0) classmethod

Estimate parameters for all neighbor list types from system geometry.

Computes conservative initial capacities based on particle density and cutoff radii. The estimates are rounded up to the next power of base to amortize future resizing.

Parameters:

Name Type Description Default
particles_per_system Table[SystemId, Array]

Number of particles per system.

required
systems Table[SystemId, NeighborListSystems]

System data with cell information.

required
cutoffs Table[SystemId, Array]

Cutoff distance per system.

required
base float

Base for power-of rounding (default 2).

2
multiplier float

Safety factor applied to the estimate (default 1.0).

1.0

Returns:

Type Description
UniversalNeighborlistParameters

A UniversalNeighborlistParameters instance with estimated values.

Source code in src/kups/core/neighborlist/parameters.py
@classmethod
@no_jax_tracing
def estimate(
    cls,
    particles_per_system: Table[SystemId, Array],
    systems: Table[SystemId, NeighborListSystems],
    cutoffs: Table[SystemId, Array],
    *,
    base: float = 2,
    multiplier: float = 1.0,
) -> UniversalNeighborlistParameters:
    """Estimate parameters for all neighbor list types from system geometry.

    Computes conservative initial capacities based on particle density
    and cutoff radii. The estimates are rounded up to the next power of
    ``base`` to amortize future resizing.

    Args:
        particles_per_system: Number of particles per system.
        systems: System data with cell information.
        cutoffs: Cutoff distance per system.
        base: Base for power-of rounding (default 2).
        multiplier: Safety factor applied to the estimate (default 1.0).

    Returns:
        A ``UniversalNeighborlistParameters`` instance with estimated values.
    """

    def _next_power(total: float | Array) -> int:
        return int(next_higher_power(jnp.array(total * multiplier), base=base))

    sys = Table.join(systems, particles_per_system, cutoffs)
    total_candidates = total_image_candidates = total_edges = max_cells = 0
    with no_post_init():
        for _, (s, n_p, c) in sys:
            n_bins = num_cells(s, c).prod()
            candidates = min(n_p / n_bins * (3**3), n_p)
            # A cutoff reaching past perp/2 replicates each candidate once per
            # periodic image (product of per-axis image counts). Summing per
            # system keeps the estimate tight for heterogeneous cutoffs instead
            # of assuming every system replicates at the maximum rate.
            images = candidate_image_counts(s.cell, c).prod()
            total_candidates += candidates
            total_image_candidates += _next_power(candidates) * images
            total_edges += _estimate_avg_num_edges(
                n_p, s.cell.volume, c, base, multiplier
            )
            max_cells = max(n_bins, max_cells)

    return UniversalNeighborlistParameters(
        avg_edges=int(total_edges // sys.size),
        avg_candidates=_next_power(total_candidates / sys.size),
        avg_image_candidates=_next_power(total_image_candidates / sys.size),
        cells=int(max_cells),
    )

all_connected_neighborlist(keys, systems, *, queries=None, queried_keys=None)

all_connected_neighborlist(
    keys: Table[ParticleId, P],
    systems: Table[SystemId, NeighborListSystems],
    *,
    queries: Table[ParticleId, P],
) -> Edges[Literal[2]]
all_connected_neighborlist(
    keys: Table[ParticleId, P],
    systems: Table[SystemId, NeighborListSystems],
    *,
    queried_keys: Index[ParticleId] | None = None,
) -> Edges[Literal[2]]

Neighbor list connecting all pairs sharing the same inclusion segment, ignoring distance.

Connects every particle pair that belongs to the same inclusion segment and has differing exclusion segment IDs. The cell is used only to compute minimum-image shifts.

Requires max_count to be set on the inclusion Index.

Source code in src/kups/core/neighborlist/all_connected.py
def all_connected_neighborlist[P: NeighborListPoints](
    keys: Table[ParticleId, P],
    systems: Table[SystemId, NeighborListSystems],
    *,
    queries: Table[ParticleId, P] | None = None,
    queried_keys: Index[ParticleId] | None = None,
) -> Edges[Literal[2]]:
    """Neighbor list connecting all pairs sharing the same inclusion segment, ignoring distance.

    Connects every particle pair that belongs to the same inclusion segment and has
    differing exclusion segment IDs. The cell is used only to compute
    minimum-image shifts.

    Requires ``max_count`` to be set on the inclusion ``Index``.
    """
    max_count = keys.data.inclusion.max_count
    assert max_count is not None, "inclusion.max_count must be set"
    query_size = (
        queried_keys.size
        if queried_keys is not None
        else (queries.size if queries is not None else keys.size)
    )
    capacity = FixedCapacity(max_count).multiply(min(keys.size, query_size))

    pipeline = Pipeline[Literal[2]](
        selector=InclusionGroupSelector(capacity=capacity),
        masks=(ExclusionMask(), QueriedKeysDedupMask()),
        compactor=ReduceCompactor(avg_edges=capacity),
        postprocessors=(MirrorPairEdges(),),
    )
    if queries is not None:
        return pipeline(keys, systems, queries=queries)
    return pipeline(keys, systems, queried_keys=queried_keys)

all_dense_cost(num_particles, num_systems)

Default cost for :class:AllDenseNearestNeighborList.

O(N^2) across all particles, so it ties dense for a single system and is invalid (inf) for multiple systems, which it would incorrectly merge.

Source code in src/kups/core/neighborlist/adaptive.py
def all_dense_cost(num_particles: int, num_systems: int) -> float:
    """Default cost for :class:`AllDenseNearestNeighborList`.

    ``O(N^2)`` across all particles, so it ties dense for a single system and is
    invalid (``inf``) for multiple systems, which it would incorrectly merge.
    """
    return num_particles**2 if num_systems <= 1 else math.inf

cell_list_cost(num_particles, num_systems)

Default cost for :class:CellListNeighborList (O(N) with a large constant, crossing dense at _CELL_LIST_CROSSOVER particles per system).

Source code in src/kups/core/neighborlist/adaptive.py
def cell_list_cost(num_particles: int, num_systems: int) -> float:
    """Default cost for :class:`CellListNeighborList` (``O(N)`` with a large
    constant, crossing dense at ``_CELL_LIST_CROSSOVER`` particles per system)."""
    return _CELL_LIST_CROSSOVER * num_particles

dense_cost(num_particles, num_systems)

Default cost for :class:DenseNearestNeighborList (O(N^2/K)).

Source code in src/kups/core/neighborlist/adaptive.py
def dense_cost(num_particles: int, num_systems: int) -> float:
    """Default cost for :class:`DenseNearestNeighborList` (``O(N^2/K)``)."""
    return num_particles**2 / max(num_systems, 1)

neighborlist_changes(neighborlist, lh, rh, systems, compaction=0.5)

Compute added/removed edges from a particle change in a single call.

Appends proposed positions to the particle array and queries both old and new interactions at once, then splits the result by filtering edge indices into removed (before) and added (after) sets.

Parameters:

Name Type Description Default
neighborlist NeighborList[Literal[2]]

Neighbor list implementation.

required
lh Table[ParticleId, NeighborListPoints]

Full original particle table.

required
rh WithIndices[ParticleId, Table[ParticleId, NeighborListPoints]]

Proposed changes — rh.indices maps entries to particle IDs in lh, rh.data holds the new particle data.

required
systems Table[SystemId, NeighborListSystems]

Per-system data (cells, etc.).

required
compaction float

Fraction of total edges allocated per output (0–1). 0.5 means each of added/removed gets half the buffer. 1.0 means no compaction — full buffer with masking only.

0.5

Returns:

Type Description
NeighborListChangesResult

NeighborListChangesResult(added, removed).

Source code in src/kups/core/neighborlist/changes.py
@jit(static_argnames=("compaction",))
def neighborlist_changes(
    neighborlist: NeighborList[Literal[2]],
    lh: Table[ParticleId, NeighborListPoints],
    rh: WithIndices[ParticleId, Table[ParticleId, NeighborListPoints]],
    systems: Table[SystemId, NeighborListSystems],
    compaction: float = 0.5,
) -> NeighborListChangesResult:
    """Compute added/removed edges from a particle change in a single call.

    Appends proposed positions to the particle array and queries both old
    and new interactions at once, then splits the result by filtering
    edge indices into ``removed`` (before) and ``added`` (after) sets.

    Args:
        neighborlist: Neighbor list implementation.
        lh: Full original particle table.
        rh: Proposed changes — ``rh.indices`` maps entries to particle IDs
            in ``lh``, ``rh.data`` holds the new particle data.
        systems: Per-system data (cells, etc.).
        compaction: Fraction of total edges allocated per output (0–1).
            0.5 means each of added/removed gets half the buffer.
            1.0 means no compaction — full buffer with masking only.

    Returns:
        ``NeighborListChangesResult(added, removed)``.
    """
    N, k = lh.size, rh.data.size
    p_idx = rh.indices.indices_in(lh.keys)

    # Build a single self-graph query with both old and proposed particles in
    # the key table. ``queried_keys`` enumerates edges incident to the changed
    # old slots and the proposed rows. Concatenating in the shifted key space
    # maps out-of-bounds (no-op) old slots to the combined OOB sentinel rather
    # than ``len(lh)``, so an empty per-system change cannot alias the first
    # appended particle and duplicate its edges.
    keys_combined = Table.union((lh, rh.data))
    queried_keys = Index.concatenate(rh.indices, rh.data.index, shift_keys=True)

    # single neighborlist call
    all_edges = neighborlist(keys_combined, systems, queried_keys=queried_keys)

    # split into removed / added
    raw = all_edges.indices.indices  # (n_edges, 2)
    c0, c1 = raw[:, 0], raw[:, 1]
    # Removed mask checks for edges that exist in the original set (both indices < N).
    removed_mask = (c0 < N) & (c1 < N)

    # is_stale mask checks that both edges need to be in the original set
    # or one needs to be in the original set and the other needs to be in the new set.
    is_stale = isin(c0, p_idx, N + k) & (c0 < N) | isin(c1, p_idx, N + k) & (c1 < N)
    # Added mask checks for edges that involve at least one new particle.
    added_mask = (c0 < N + k) & (c1 < N + k) & ((c0 >= N) | (c1 >= N)) & ~is_stale

    # remap appended indices N+m -> p_idx[m]
    remapped = jnp.where(raw >= N, p_idx[raw - N], raw)

    # compact each output
    n_total = raw.shape[0]
    shifts = all_edges.shifts

    def _mask_only(mask: Array, indices: Array, shifts: Array) -> Edges[Literal[2]]:
        idx = where_broadcast_last(mask, indices, N)
        sh = where_broadcast_last(mask, shifts, 0)
        return Edges(Index(lh.keys, idx), sh)

    def _compact(mask: Array, indices: Array, label: str) -> Edges[Literal[2]]:
        count = mask.sum()
        runtime_assert(
            count <= capacity,
            f"neighborlist_changes: {label} edges ({{count}}) exceed "
            f"capacity ({{capacity}})",
            fmt_args={"count": count, "capacity": jnp.array(capacity)},
        )
        sel: Array = jnp.where(mask, size=capacity, fill_value=n_total - 1)[0]
        valid = mask.at[sel].get(mode="fill", fill_value=False)
        return _mask_only(valid, indices[sel], shifts[sel])

    if compaction >= 1.0:
        return NeighborListChangesResult(
            _mask_only(added_mask, remapped, shifts),
            _mask_only(removed_mask, remapped, shifts),
        )

    capacity = int(n_total * compaction)
    return NeighborListChangesResult(
        _compact(added_mask, remapped, "added"),
        _compact(removed_mask, remapped, "removed"),
    )