Skip to content

kups.core.neighborlist.common

Shared algorithmic helpers for neighbor list selectors and masks.

Contains:

  • num_cells — per-axis spatial bin counts (used by the cell-list selector and by parameters.estimate).
  • Candidates — private intermediate struct used inside individual selector algorithms while raw (key_idx, query_idx) index arrays are being built. Not the pipeline carrier (see CandidateBatch).
  • candidate_image_counts — per-axis periodic-image window width a cutoff reaches; used by _get_candidate_images and by parameters.estimate.
  • _generate_image_offsets, _get_candidate_images — image-expansion primitives (per-pair anchored windows).
  • replicate_for_images — adapts raw Candidates into a CandidateBatch with shifts and is_minimum_image set, replicating each pair across its anchored image window when cutoff > perp/2.
  • make_batch_with_mic — pack raw candidates with minimum-image shifts and is_minimum_image=all-True (used by selectors that don't replicate).
  • real_distance_sq — squared real-space distance between candidate pairs given fractional shifts; used by DistanceCutoffMask.

Candidates

Private intermediate produced inside selector algorithms.

Not the pipeline carrier — selectors convert Candidates into a CandidateBatch (via replicate_for_images or make_batch_with_mic) before returning.

Source code in src/kups/core/neighborlist/common.py
@dataclass
class Candidates:
    """Private intermediate produced inside selector algorithms.

    Not the pipeline carrier — selectors convert ``Candidates`` into a
    ``CandidateBatch`` (via ``replicate_for_images`` or
    ``make_batch_with_mic``) before returning.
    """

    key_idx: Index[ParticleId]
    query_idx: Index[ParticleId]

candidate_image_counts(cells, cutoffs)

Return per-system, per-axis periodic-image window widths.

Each periodic axis needs ceil(2 * cutoff / perpendicular_length) consecutive integer shifts -- the tight count of lattice planes a sphere of radius cutoff can reach along that axis under the strict < cutoff distance mask (the max number of integers in an open interval of that width). At ratio <= 0.5 this collapses to one image, recovering the minimum-image convention. Open axes and non-finite ratios use one image. perpendicular_lengths is the correct per-axis measure for arbitrary skew (it equals 1 / |column of inverse_vectors|).

Source code in src/kups/core/neighborlist/common.py
def candidate_image_counts(cells: Cell[AnyPeriodicity], cutoffs: Array) -> Array:
    """Return per-system, per-axis periodic-image window widths.

    Each periodic axis needs ``ceil(2 * cutoff / perpendicular_length)``
    consecutive integer shifts -- the tight count of lattice planes a sphere of
    radius ``cutoff`` can reach along that axis under the strict ``< cutoff``
    distance mask (the max number of integers in an open interval of that
    width). At ``ratio <= 0.5`` this collapses to one image, recovering the
    minimum-image convention. Open axes and non-finite ratios use one image.
    ``perpendicular_lengths`` is the correct per-axis measure for arbitrary
    skew (it equals ``1 / |column of inverse_vectors|``).
    """
    ratio = cutoffs[..., None] / cells.perpendicular_lengths
    images = jnp.maximum(jnp.ceil(2 * ratio), 1).astype(int)
    images = jnp.where(jnp.isfinite(ratio), images, 1)
    return jnp.where(jnp.array(cells.periodic), images, 1)

candidates_to_batch(candidates, shifts, is_minimum_image)

Pack (candidates, flat shifts, is_min) into a CandidateBatch[2].

Source code in src/kups/core/neighborlist/common.py
def candidates_to_batch(
    candidates: Candidates,
    shifts: Array,
    is_minimum_image: Array,
) -> CandidateBatch[Literal[2]]:
    """Pack ``(candidates, flat shifts, is_min)`` into a ``CandidateBatch[2]``."""
    indices_2d = jnp.stack(
        [candidates.key_idx.indices, candidates.query_idx.indices], axis=-1
    )
    edges: Edges[Literal[2]] = Edges(
        Index(candidates.key_idx.keys, indices_2d),
        jnp.expand_dims(shifts, axis=-2),
    )
    return CandidateBatch(
        edges=edges,
        is_minimum_image=is_minimum_image,
        query_keys=candidates.query_idx.keys,
    )

lift_query_candidates(candidates, ctx)

Convert query-local self-update candidates to ctx.keys positions.

Source code in src/kups/core/neighborlist/common.py
def lift_query_candidates(candidates: Candidates, ctx: PipelineContext) -> Candidates:
    """Convert query-local self-update candidates to ``ctx.keys`` positions."""
    if ctx.queried_keys is None:
        return candidates
    oob = ctx.keys.size
    query_idx = ctx.queried_keys.at[candidates.query_idx.indices].get(
        mode="fill", fill_value=oob
    )
    return Candidates(
        key_idx=candidates.key_idx, query_idx=Index(ctx.keys.keys, query_idx)
    )

make_batch_with_mic(candidates, keys, queries, systems)

Pack raw candidates with minimum-image shifts; is_minimum_image=all-True.

Source code in src/kups/core/neighborlist/common.py
def make_batch_with_mic(
    candidates: Candidates,
    keys: Table[ParticleId, NeighborListPoints],
    queries: Table[ParticleId, NeighborListPoints],
    systems: Table[SystemId, NeighborListSystems],
) -> CandidateBatch[Literal[2]]:
    """Pack raw candidates with minimum-image shifts; ``is_minimum_image=all-True``."""
    min_shifts = _minimum_image_shifts(candidates, keys, queries, systems)
    return candidates_to_batch(
        candidates,
        min_shifts,
        jnp.ones((candidates.key_idx.size,), dtype=bool),
    )

real_distance_sq(key_positions, query_positions, frames, shifts)

Squared real-space distance between already-broadcast candidate pairs.

Parameters:

Name Type Description Default
key_positions Array

Fractional left endpoint positions, shape (n, 3).

required
query_positions Array

Fractional right endpoint positions, shape (n, 3).

required
frames MaterializedFrame

Materialized cell frames broadcast to the candidate key system.

required
shifts Array

(n, 3) fractional shifts.

required

Returns:

Type Description
Array

(n,) array of squared distances in real coordinates.

Source code in src/kups/core/neighborlist/common.py
def real_distance_sq(
    key_positions: Array,
    query_positions: Array,
    frames: MaterializedFrame,
    shifts: Array,
) -> Array:
    """Squared real-space distance between already-broadcast candidate pairs.

    Args:
        key_positions: Fractional left endpoint positions, shape ``(n, 3)``.
        query_positions: Fractional right endpoint positions, shape ``(n, 3)``.
        frames: Materialized cell frames broadcast to the candidate key system.
        shifts: ``(n, 3)`` fractional shifts.

    Returns:
        ``(n,)`` array of squared distances in real coordinates.
    """
    deltas = key_positions - query_positions - shifts
    real_deltas = frames.to_real(deltas)
    return jnp.einsum("...d,...d->...", real_deltas, real_deltas)

replicate_for_images(candidates, keys, queries, systems, cutoffs, max_image_candidates)

Replicate candidates across their periodic-image windows.

For each candidate pair: - If cutoff[sys] / perp_axes <= 0.5 on every axis: emit 1 copy with MIC shifts (the minimum image is the only image in range). - Otherwise: emit the per-axis window of integer shifts anchored at the pair's separation (see _get_candidate_images); is_minimum_image flags the closest copy per pair (via real-distance argmin) so ExclusionMask keeps non-minimum image periodic copies of excluded pairs. Over-emitted copies are pruned downstream by DistanceCutoffMask.

Parameters:

Name Type Description Default
candidates Candidates

Raw candidate pair indices.

required
keys, queries, systems

Pipeline tables (fractional coords).

required
cutoffs Table[SystemId, Array]

Per-system cutoff.

required
max_image_candidates Capacity[int] | None

Capacity for replicated-candidates buffer. When None, falls back to FixedCapacity(candidates.key_idx.size) with an error message — pass an editable capacity if image replication is expected.

required

Returns:

Type Description
CandidateBatch[Literal[2]]

CandidateBatch with shifts populated and is_minimum_image set.

Source code in src/kups/core/neighborlist/common.py
def replicate_for_images(
    candidates: Candidates,
    keys: Table[ParticleId, NeighborListPoints],
    queries: Table[ParticleId, NeighborListPoints],
    systems: Table[SystemId, NeighborListSystems],
    cutoffs: Table[SystemId, Array],
    max_image_candidates: Capacity[int] | None,
) -> CandidateBatch[Literal[2]]:
    """Replicate candidates across their periodic-image windows.

    For each candidate pair:
    - If ``cutoff[sys] / perp_axes <= 0.5`` on every axis: emit 1 copy with MIC
      shifts (the minimum image is the only image in range).
    - Otherwise: emit the per-axis window of integer shifts anchored at the pair's
      separation (see ``_get_candidate_images``); ``is_minimum_image`` flags the
      closest copy per pair (via real-distance argmin) so ``ExclusionMask`` keeps
      non-minimum image periodic copies of excluded pairs. Over-emitted copies are
      pruned downstream by ``DistanceCutoffMask``.

    Args:
        candidates: Raw candidate pair indices.
        keys, queries, systems: Pipeline tables (fractional coords).
        cutoffs: Per-system cutoff.
        max_image_candidates: Capacity for replicated-candidates buffer.
            When ``None``, falls back to ``FixedCapacity(candidates.key_idx.size)``
            with an error message — pass an editable capacity if image
            replication is expected.

    Returns:
        ``CandidateBatch`` with shifts populated and ``is_minimum_image`` set.
    """
    cutoffs_t = Table.broadcast_to(cutoffs, systems)
    if max_image_candidates is None:
        max_image_candidates = FixedCapacity(
            candidates.key_idx.size,
            "Cutoff is larger than half the cell length, "
            "we need to generate additional images. "
            "Please provide a editable max_candidates.",
        )

    idx, offsets = _get_candidate_images(
        candidates, keys, queries, systems, cutoffs_t.data, max_image_candidates
    )

    if idx.size == candidates.key_idx.size:
        # No replication needed — MIC shifts cover everything.
        min_shifts = _minimum_image_shifts(candidates, keys, queries, systems)
        return candidates_to_batch(
            candidates, min_shifts, jnp.ones((candidates.key_idx.size,), dtype=bool)
        )

    replicated = bind(candidates).at(idx).get()
    is_min = _minimum_image_mask(
        replicated, offsets, idx, keys, queries, systems, candidates.key_idx.size
    )
    return candidates_to_batch(replicated, offsets, is_min)