Source code for nvalchemiops.neighbors.neighbor_utils

# SPDX-FileCopyrightText: Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Core warp utilities for neighbor list construction.

This module contains warp kernels and launchers for neighbor list operations.
See `nvalchemiops.torch.neighbors` for PyTorch bindings.
"""

import contextlib
import math
import warnings
from functools import lru_cache
from typing import Any

import warp as wp

DTYPE_INFO_ALL: dict[type, tuple[type, type]] = {
    wp.float16: (wp.vec3h, wp.mat33h),
    wp.float32: (wp.vec3f, wp.mat33f),
    wp.float64: (wp.vec3d, wp.mat33d),
}

_DTYPE_NAME: dict[type, str] = {
    wp.float16: "f16",
    wp.float32: "f32",
    wp.float64: "f64",
}


def wp_device_str(device) -> str:
    """Return Warp's canonical device alias for cache keys and allocations."""
    return str(wp.get_device(device))


def require_supported_dtype(
    wp_dtype: type, allowed: tuple[type, ...] | None = None
) -> None:
    """Validate that ``wp_dtype`` is a supported Warp scalar dtype."""
    allowed_dtypes = tuple(DTYPE_INFO_ALL) if allowed is None else allowed
    if wp_dtype not in allowed_dtypes:
        names = ", ".join(str(dtype) for dtype in allowed_dtypes)
        raise ValueError(f"Unsupported dtype {wp_dtype!r}; expected one of: {names}")


def dtype_info(
    wp_dtype: type, allowed: tuple[type, ...] | None = None
) -> tuple[type, type]:
    """Return ``(vec_dtype, mat_dtype)`` for a supported scalar dtype."""
    require_supported_dtype(wp_dtype, allowed)
    return DTYPE_INFO_ALL[wp_dtype]


def kernel_specialization_name(
    base: str,
    *,
    wp_dtype: type | None = None,
    features: tuple[str, ...] = (),
) -> str:
    """Return a stable name for a factory-created Warp specialization."""
    tokens = tuple(str(feature) for feature in features if feature)
    name = str(base)
    if tokens:
        name = f"{name}__{'_'.join(tokens)}"
    if wp_dtype is not None:
        require_supported_dtype(wp_dtype)
        name = f"{name}__{_DTYPE_NAME[wp_dtype]}"
    return name


def set_fn_name(fn: Any, name: str) -> Any:
    """Set Python- and Warp-visible names on a generated function object."""
    fn.__name__ = name
    fn.__qualname__ = name
    if hasattr(fn, "key"):
        old_key = fn.key
        fn.key = name
        # If this is a Warp kernel/function registered in a module, update its registration key
        if hasattr(fn, "module") and fn.module is not None:
            if hasattr(fn.module, "kernels") and old_key in fn.module.kernels:
                del fn.module.kernels[old_key]
                fn.module.kernels[name] = fn
            elif hasattr(fn.module, "functions") and old_key in fn.module.functions:
                del fn.module.functions[old_key]
                fn.module.functions[name] = fn
            # If this is a unique module, rename the module itself to match the new kernel name
            if hasattr(fn.module, "name") and fn.module.name:
                old_module_name = fn.module.name
                if old_key in old_module_name:
                    new_module_name = old_module_name.replace(old_key, name)
                    from warp._src.context import user_modules

                    if old_module_name in user_modules:
                        del user_modules[old_module_name]
                        user_modules[new_module_name] = fn.module
                    fn.module.name = new_module_name
            # Clear module hashers cache to force hash re-evaluation and rebuild on all platforms
            if hasattr(fn.module, "hashers"):
                fn.module.hashers.clear()
        # Force recomputing kernel hash using the new key if it has one
        # (best-effort: a Warp-internal change here must not break naming).
        if hasattr(fn, "hash"):
            with contextlib.suppress(Exception):
                opts = (
                    fn.module.options
                    if (hasattr(fn, "module") and fn.module is not None)
                    else {}
                )
                hasher = wp._src.context.ModuleHasher([], opts)
                fn.hash = hasher.hash_kernel(fn)
    wrapped = getattr(fn, "func", None)
    if wrapped is not None:
        wrapped.__name__ = name
        wrapped.__qualname__ = name
    return fn


def set_fn_doc(fn: Any, doc: str) -> Any:
    """Set Python- and Warp-visible docs on a generated function object."""
    fn.__doc__ = doc
    if hasattr(fn, "doc"):
        fn.doc = doc
    wrapped = getattr(fn, "func", None)
    if wrapped is not None:
        wrapped.__doc__ = doc
    return fn


def _append_specialization_doc(
    base_doc: str | None,
    *,
    dtype: type | str | None = None,
    entries: tuple[tuple[str, object], ...] = (),
) -> str:
    """Append runtime specialization metadata to a source docstring."""
    doc = (base_doc or "").rstrip()
    lines = ["", "Specialization", "--------------"]
    if dtype is not None:
        dtype_value = _DTYPE_NAME.get(dtype, dtype)
        lines.append(f"dtype : {dtype_value}")
    for name, value in entries:
        lines.append(f"{name} : {value}")
    return f"{doc}\n" + "\n".join(lines)


def resolve_buffer_alias(new_name, new_value, old_name, old_value):
    """Resolve a deprecated scratch-buffer kwarg alias.

    Returns the active value, emitting a :class:`DeprecationWarning` if the
    caller used the old unsuffixed name.  Raises ``ValueError`` if both names
    are populated.
    """
    if old_value is None:
        return new_value
    warnings.warn(
        f"The {old_name!r} kwarg is deprecated; use {new_name!r} instead.",
        DeprecationWarning,
        stacklevel=3,
    )
    if new_value is not None:
        raise ValueError(f"Pass either {new_name!r} or {old_name!r}, not both.")
    return old_value


def empty_sentinel(ndim: int, dtype: type, device) -> wp.array:
    """Return a cached zero-size sentinel array for ``ndim``/``dtype``/``device``."""
    return _empty_sentinel_cached(int(ndim), dtype, wp_device_str(device))


@lru_cache(maxsize=None)
def _empty_sentinel_cached(ndim: int, dtype: type, device: str) -> wp.array:
    """Allocate a cached zero-size sentinel array for a canonical device alias."""
    return wp.empty((0,) * ndim, dtype=dtype, device=device)


_empty_sentinel = empty_sentinel


[docs] class NeighborOverflowError(Exception): """Exception raised when a neighbor output exceeds its capacity. This error indicates that a pre-allocated neighbor matrix or COO segment is too small to hold all discovered neighbors. Users should increase the relevant ``max_neighbors`` / segment-capacity parameter or provide a larger pre-allocated tensor. Parameters ---------- max_neighbors : int The maximum number of neighbors or COO entries the output can hold. num_neighbors : int The actual number of neighbors or COO entries found. system_index : int, optional System index for segmented batched outputs. """ def __init__( self, max_neighbors: int, num_neighbors: int, system_index: int | None = None ): if system_index is None: message = ( "The number of neighbors is larger than the maximum allowed: " f"{num_neighbors} > {max_neighbors}." ) else: message = ( f"The number of neighbors in segment {system_index} is larger " f"than the maximum allowed: {num_neighbors} > {max_neighbors}." ) super().__init__(message) self.max_neighbors = max_neighbors self.num_neighbors = num_neighbors self.system_index = system_index
__all__ = [ "DTYPE_INFO_ALL", "NeighborOverflowError", "dtype_info", "empty_sentinel", "compute_naive_num_shifts", "compute_inv_cells", "estimate_max_neighbors", "fill_neighbor_matrix_tail", "get_compute_inv_cells_kernel", "get_compute_naive_num_shifts_kernel", "get_gather_positions_and_shifts_kernel", "get_update_ref_positions_kernel", "get_wrap_positions_kernel", "kernel_specialization_name", "require_supported_dtype", "resolve_buffer_alias", "selective_zero_num_neighbors", "selective_zero_num_neighbors_single", "set_fn_name", "update_ref_positions", "update_ref_positions_batch", "wrap_positions_single", "wrap_positions_batch", "wp_device_str", "zero_array", ] def zero_array(array: wp.array, device: str) -> None: """Zero all elements of a Warp array in place. .. deprecated:: Use ``array.zero_()`` directly. This shim forwards to it and will be removed in a future release. Parameters ---------- array : wp.array, dtype=Any OUTPUT: Array to be zeroed in place. device : str Accepted for backward compatibility and ignored; ``array.zero_()`` runs on the array's own device. """ warnings.warn( "nvalchemiops.neighbors.zero_array is deprecated; use array.zero_() instead.", DeprecationWarning, stacklevel=2, ) del device # retained only for signature compatibility array.zero_() @wp.func def _decode_shift_index(local_idx: int, shift_range: wp.vec3i) -> wp.vec3i: """Decode a flat shift index into (kx, ky, kz) lattice shift vector Decodes the half-shell enumeration used by the naive PBC kernels so shift vectors can be computed on-the-fly without materialising the full shifts array. Parameters ---------- local_idx : int Zero-based index into the per-system shift enumeration. shift_range : wp.vec3i Shift range in each dimension (from ``_compute_naive_num_shifts``). Returns ------- wp.vec3i The integer lattice shift vector ``(kx, ky, kz)``. """ k2_size = 2 * shift_range[2] + 1 k1_size = 2 * shift_range[1] + 1 group0_size = shift_range[1] * k2_size + shift_range[2] + 1 k0 = wp.int32(0) k1 = wp.int32(0) k2 = wp.int32(0) if local_idx < group0_size: if local_idx <= shift_range[2]: k2 = local_idx else: rem = local_idx - (shift_range[2] + 1) k1 = rem / k2_size + 1 k2 = rem % k2_size - shift_range[2] else: rem = local_idx - group0_size k0 = rem / (k1_size * k2_size) + 1 rem2 = rem % (k1_size * k2_size) k1 = rem2 / k2_size - shift_range[1] k2 = rem2 % k2_size - shift_range[2] return wp.vec3i(k0, k1, k2) @wp.func def _decode_full_shift_index(local_idx: int, shift_range: wp.vec3i) -> wp.vec3i: """Decode a flat full-shell index into ``(kx, ky, kz)``, excluding ``(0, 0, 0)`` Companion to :func:`_decode_shift_index`. Enumerates the FULL sphere of shift vectors at radius ``shift_range`` (not the half-shell), in the natural Cartesian order (``k0`` outer, ``k2`` inner), skipping the self entry at the centre. Parameters ---------- local_idx : int Zero-based index in ``[0, (2*Rx+1)*(2*Ry+1)*(2*Rz+1) - 1)``. shift_range : wp.vec3i Per-axis radius ``(Rx, Ry, Rz)``. Returns ------- wp.vec3i The integer lattice shift vector ``(kx, ky, kz)`` with ``-Rx <= kx <= Rx`` etc., and never ``(0, 0, 0)``. """ k1_size = 2 * shift_range[1] + 1 k2_size = 2 * shift_range[2] + 1 plane = k1_size * k2_size self_pos = shift_range[0] * plane + shift_range[1] * k2_size + shift_range[2] raw_idx = local_idx if local_idx >= self_pos: raw_idx = local_idx + 1 k0 = raw_idx / plane - shift_range[0] rem = raw_idx % plane k1 = rem / k2_size - shift_range[1] k2 = rem % k2_size - shift_range[2] return wp.vec3i(k0, k1, k2) @wp.func def _shifted_position(shift: wp.vec3i, cell: Any, position: Any): """Position translated by lattice shift ``shift`` under cell matrix ``cell`` Parameters ---------- shift : wp.vec3i Integer lattice shift vector. cell : wp.mat33* Cell matrix used to convert ``shift`` to Cartesian displacement. position : wp.vec3* Cartesian position to translate. Returns ------- wp.vec3* Translated Cartesian position. Notes ----- The cell-element scalar type is recovered from ``cell[0]`` so the cast of ``shift`` matches the position dtype. """ return type(cell[0])(shift) * cell + position @wp.func def _update_dual_neighbor_matrix( i: int, j: int, dist_sq: Any, cutoff1_sq: Any, cutoff2_sq: Any, neighbor_matrix1: wp.array(dtype=wp.int32, ndim=2), neighbor_matrix_shifts1: wp.array(dtype=wp.vec3i, ndim=2), num_neighbors1: wp.array(dtype=wp.int32), max_neighbors1: int, neighbor_matrix2: wp.array(dtype=wp.int32, ndim=2), neighbor_matrix_shifts2: wp.array(dtype=wp.vec3i, ndim=2), num_neighbors2: wp.array(dtype=wp.int32), max_neighbors2: int, unit_shift: wp.vec3i, half_fill: bool, pbc: bool, ): """Update primary and secondary dual-cutoff neighbor matrices Parameters ---------- i : int Source atom row. j : int Neighbor atom index. dist_sq : float Squared pair distance. cutoff1_sq : float Squared primary cutoff distance. cutoff2_sq : float Squared secondary cutoff distance. neighbor_matrix1 : wp.array, shape (rows, max_neighbors1), dtype=wp.int32 OUTPUT: Primary cutoff neighbor matrix. neighbor_matrix_shifts1 : wp.array, shape (rows, max_neighbors1), dtype=wp.vec3i OUTPUT: Primary cutoff shift matrix for PBC mode. num_neighbors1 : wp.array, shape (rows,), dtype=wp.int32 MODIFIED: Primary cutoff neighbor counts. max_neighbors1 : int Maximum primary cutoff neighbors per row. neighbor_matrix2 : wp.array, shape (rows, max_neighbors2), dtype=wp.int32 OUTPUT: Secondary cutoff neighbor matrix. neighbor_matrix_shifts2 : wp.array, shape (rows, max_neighbors2), dtype=wp.vec3i OUTPUT: Secondary cutoff shift matrix for PBC mode. num_neighbors2 : wp.array, shape (rows,), dtype=wp.int32 MODIFIED: Secondary cutoff neighbor counts. max_neighbors2 : int Maximum secondary cutoff neighbors per row. unit_shift : wp.vec3i Periodic unit shift stored with PBC neighbor pairs. half_fill : bool If True, store only one direction for each unordered pair. pbc : bool If True, write shift matrices alongside atom indices. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Modifies: neighbor matrices, shift matrices in PBC mode, and neighbor counts. """ if dist_sq < cutoff2_sq: _update_neighbor_matrix( i, j, neighbor_matrix2, neighbor_matrix_shifts2, num_neighbors2, unit_shift, max_neighbors2, half_fill, pbc, ) if dist_sq < cutoff1_sq: _update_neighbor_matrix( i, j, neighbor_matrix1, neighbor_matrix_shifts1, num_neighbors1, unit_shift, max_neighbors1, half_fill, pbc, ) @wp.func def _correct_shift( shift: wp.vec3i, offset_i: wp.vec3i, offset_j: wp.vec3i, ) -> wp.vec3i: """Apply wrap-on-entry shift correction Parameters ---------- shift : wp.vec3i Periodic image shift before wrap-on-entry correction. offset_i : wp.vec3i Integer cell offset for the source atom. offset_j : wp.vec3i Integer cell offset for the neighbor atom. Returns ------- wp.vec3i Corrected periodic shift vector. Notes ----- The returned shift is adjusted by ``offset_i - offset_j`` so the reconstructed displacement matches the original unwrapped geometry. """ return wp.vec3i( shift[0] - offset_j[0] + offset_i[0], shift[1] - offset_j[1] + offset_i[1], shift[2] - offset_j[2] + offset_i[2], ) @wp.func def _update_neighbor_matrix( i: int, j: int, neighbor_matrix: wp.array(dtype=wp.int32, ndim=2), neighbor_matrix_shifts: wp.array(dtype=wp.vec3i, ndim=2), num_neighbors: wp.array(dtype=wp.int32), unit_shift: wp.vec3i, max_neighbors: int, half_fill: bool, pbc: bool, ): """Update the neighbor matrix with the given atom indices Parameters ---------- i: int The index of the source atom. j: int The index of the target atom. neighbor_matrix: wp.array(dtype=wp.int32, ndim=2) OUTPUT: The neighbor matrix to be updated. neighbor_matrix_shifts: wp.array(dtype=wp.vec3i, ndim=2) OUTPUT: The neighbor matrix shifts to be updated when ``pbc`` is true. num_neighbors: wp.array(dtype=wp.int32) OUTPUT: The number of neighbors for each atom. unit_shift: wp.vec3i The unit shift vector for the periodic boundary. max_neighbors: int The maximum number of neighbors for each atom. half_fill: bool If True, only fill half of the neighbor matrix. pbc: bool If True, write periodic shift entries alongside atom indices. Returns ------- None This function modifies the input arrays in-place. """ pos = wp.atomic_add(num_neighbors, i, 1) if pos < max_neighbors: neighbor_matrix[i, pos] = j if pbc: neighbor_matrix_shifts[i, pos] = unit_shift if not half_fill and (pbc or i < j): pos = wp.atomic_add(num_neighbors, j, 1) if pos < max_neighbors: neighbor_matrix[j, pos] = i if pbc: neighbor_matrix_shifts[j, pos] = -unit_shift @wp.kernel(enable_backward=False) def _compute_naive_num_shifts( cell: wp.array(dtype=Any), cutoff: Any, pbc: wp.array2d(dtype=wp.bool), num_shifts: wp.array(dtype=int), shift_range: wp.array(dtype=wp.vec3i), ) -> None: """Compute periodic image shifts needed for neighbor searching Calculates the number and range of periodic boundary shifts required to ensure all atoms within the cutoff distance are found, taking into account the geometry of the simulation cell and minimum image convention. Parameters ---------- cell : wp.array, shape (num_systems, 3, 3), dtype=wp.mat33* Cell matrices defining lattice vectors in Cartesian coordinates. Each 3x3 matrix represents one system's periodic cell. cutoff : float Cutoff distance for neighbor searching in Cartesian units. Must be positive and typically less than half the minimum cell dimension. pbc : wp.array, shape (num_systems, 3), dtype=wp.bool Periodic boundary condition flags for each dimension. True enables periodicity in that direction. num_shifts : wp.array, shape (num_systems,), dtype=int OUTPUT: Total number of periodic shifts needed for each system. Updated with calculated shift counts. shift_range : wp.array, shape (num_systems, 3), dtype=wp.vec3i OUTPUT: Maximum shift indices in each dimension for each system. Updated with calculated shift ranges. Returns ------- None This function modifies the input arrays in-place: - num_shifts : Updated with total shift counts per system - shift_range : Updated with shift ranges per dimension Notes ----- - Thread launch: see launcher-specific launch dimension. - Modifies: see OUTPUT or MODIFIED parameters. See Also -------- get_compute_naive_num_shifts_kernel : Return the specialized periodic-image shift-count kernel. """ tid = wp.tid() _cell = cell[tid] _pbc = pbc[tid] _cell_inv = wp.transpose(wp.inverse(_cell)) _d_inv_0 = wp.length(_cell_inv[0]) if _pbc[0] else type(_cell_inv[0, 0])(0.0) _d_inv_1 = wp.length(_cell_inv[1]) if _pbc[1] else type(_cell_inv[1, 0])(0.0) _d_inv_2 = wp.length(_cell_inv[2]) if _pbc[2] else type(_cell_inv[2, 0])(0.0) _s = wp.vec3i( wp.int32(wp.ceil(_d_inv_0 * type(_d_inv_0)(cutoff))), wp.int32(wp.ceil(_d_inv_1 * type(_d_inv_1)(cutoff))), wp.int32(wp.ceil(_d_inv_2 * type(_d_inv_2)(cutoff))), ) k1 = 2 * _s[1] + 1 k2 = 2 * _s[2] + 1 shift_range[tid] = _s num_shifts[tid] = _s[0] * k1 * k2 + _s[1] * k2 + _s[2] + 1 @lru_cache(maxsize=None) def get_compute_naive_num_shifts_kernel(wp_dtype: type) -> wp.Kernel: """Return the specialized periodic-image shift-count kernel.""" _vec_dtype, mat_dtype = dtype_info(wp_dtype) kernel = wp.overload( _compute_naive_num_shifts, [ wp.array(dtype=mat_dtype), wp_dtype, wp.array2d(dtype=wp.bool), wp.array(dtype=int), wp.array(dtype=wp.vec3i), ], ) name = kernel_specialization_name("_compute_naive_num_shifts", wp_dtype=wp_dtype) return set_fn_doc( set_fn_name(kernel, name), _append_specialization_doc( kernel.__doc__, dtype=wp_dtype, entries=(("operation", "compute_naive_num_shifts"),), ), ) def _make_selective_zero_num_neighbors_kernel(*, batched: bool): """Build the selective ``num_neighbors`` zeroing kernel.""" BATCHED = wp.constant(bool(batched)) @wp.kernel(enable_backward=False, module="unique") def _kernel( num_neighbors: wp.array(dtype=wp.int32), batch_idx: wp.array(dtype=wp.int32), rebuild_flags: wp.array(dtype=wp.bool), ) -> None: """Zero neighbor counts for atoms in rebuilt systems Parameters ---------- num_neighbors : wp.array, shape (total_atoms,), dtype=wp.int32 OUTPUT: Per-atom neighbor counts to zero selectively. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. Zero-size sentinel in single-system specializations. rebuild_flags : wp.array, shape (num_systems,), dtype=wp.bool Rebuild flags controlling which atoms have their counts reset. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Thread launch: One thread per atom. - Modifies: ``num_neighbors`` entries for rebuilt systems. ``BATCHED`` is a static specialization. Single-system launchers pass a zero-size ``batch_idx`` sentinel that is not read. See Also -------- _get_selective_zero_num_neighbors_kernel : Return the specialized selective neighbor-count zeroing kernel. """ tid = wp.tid() isys = wp.int32(0) if BATCHED: isys = batch_idx[tid] if rebuild_flags[isys]: num_neighbors[tid] = 0 base = ( "_selective_zero_num_neighbors" if batched else "_selective_zero_num_neighbors_single" ) name = kernel_specialization_name(base) return set_fn_doc( set_fn_name(_kernel, name), _append_specialization_doc( _kernel.__doc__, entries=( ("batched", bool(batched)), ("operation", "selective_zero_num_neighbors"), ), ), ) @lru_cache(maxsize=None) def _get_selective_zero_num_neighbors_kernel(*, batched: bool) -> wp.Kernel: """Return the selective ``num_neighbors`` zeroing kernel for batching.""" return _make_selective_zero_num_neighbors_kernel(batched=bool(batched)) def selective_zero_num_neighbors( num_neighbors: wp.array, batch_idx: wp.array, rebuild_flags: wp.array, device: str, ) -> None: """Core warp launcher for selectively zeroing num_neighbors. Zeros the num_neighbors count for atoms belonging to systems where rebuild_flags is True, preserving counts for non-rebuilt systems. Parameters ---------- num_neighbors : wp.array, shape (total_atoms,), dtype=wp.int32 OUTPUT: Per-atom neighbor counts; selectively zeroed. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. rebuild_flags : wp.array, shape (num_systems,), dtype=wp.bool Per-system flags indicating which systems need rebuilding. device : str Warp device string (e.g., 'cuda:0', 'cpu'). See Also -------- _get_selective_zero_num_neighbors_kernel : Selects the batched kernel """ total_atoms = num_neighbors.shape[0] wp.launch( kernel=_get_selective_zero_num_neighbors_kernel(batched=True), dim=total_atoms, inputs=[num_neighbors, batch_idx, rebuild_flags], device=device, ) def selective_zero_num_neighbors_single( num_neighbors: wp.array, rebuild_flags: wp.array, device: str, ) -> None: """Core warp launcher for selectively zeroing num_neighbors for a single system. Zeros all num_neighbors entries when rebuild_flags[0] is True. When False the kernel returns immediately — no CPU-GPU synchronization occurs. Parameters ---------- num_neighbors : wp.array, shape (total_atoms,), dtype=wp.int32 OUTPUT: Per-atom neighbor counts; zeroed when rebuild is needed. rebuild_flags : wp.array, shape (1,) or shape (), dtype=wp.bool Single-system rebuild flag. device : str Warp device string (e.g., 'cuda:0', 'cpu'). See Also -------- _get_selective_zero_num_neighbors_kernel : Selects the single-system kernel selective_zero_num_neighbors : Batch variant using per-atom batch_idx """ total_atoms = num_neighbors.shape[0] wp.launch( kernel=_get_selective_zero_num_neighbors_kernel(batched=False), dim=total_atoms, inputs=[ num_neighbors, _empty_sentinel(1, wp.int32, device), rebuild_flags, ], device=device, ) @wp.kernel(enable_backward=False) def _compute_inv_cells_kernel( cell: wp.array(dtype=Any), inv_cell: wp.array(dtype=Any), ) -> None: """Compute the inverse of each cell matrix Parameters ---------- cell : wp.array, shape (num_systems,), dtype=wp.mat33* Input cell matrices. inv_cell : wp.array, shape (num_systems,), dtype=wp.mat33* OUTPUT: Inverse of each cell matrix. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Modifies: see OUTPUT or MODIFIED parameters. - Thread launch: One thread per system (dim=num_systems) See Also -------- get_compute_inv_cells_kernel : Return the specialized inverse-cell kernel. """ tid = wp.tid() inv_cell[tid] = wp.inverse(cell[tid]) @lru_cache(maxsize=None) def get_compute_inv_cells_kernel(wp_dtype: type) -> wp.Kernel: """Return the specialized inverse-cell kernel.""" _vec_dtype, mat_dtype = dtype_info(wp_dtype) kernel = wp.overload( _compute_inv_cells_kernel, [wp.array(dtype=mat_dtype), wp.array(dtype=mat_dtype)], ) name = kernel_specialization_name("_compute_inv_cells_kernel", wp_dtype=wp_dtype) return set_fn_doc( set_fn_name(kernel, name), _append_specialization_doc( kernel.__doc__, dtype=wp_dtype, entries=(("operation", "compute_inv_cells"),), ), ) def compute_inv_cells( cell: wp.array, inv_cell: wp.array, wp_dtype: type, device: str, ) -> None: """Core warp launcher for computing inverse cell matrices. Inverts each cell matrix in the batch using pure warp operations. Call this once before launching naive PBC neighbor-list kernels to avoid redundant per-thread inversions inside those kernels. Parameters ---------- cell : wp.array, shape (num_systems,), dtype=wp.mat33* Input cell matrices. inv_cell : wp.array, shape (num_systems,), dtype=wp.mat33* OUTPUT: Inverse of each cell matrix. Must be pre-allocated with the same shape and dtype as *cell*. wp_dtype : type Warp scalar dtype (wp.float32, wp.float64, or wp.float16). device : str Warp device string (e.g., ``'cuda:0'``, ``'cpu'``). See Also -------- get_compute_inv_cells_kernel : Factory-selected inverse-cell kernel """ num_systems = cell.shape[0] wp.launch( kernel=get_compute_inv_cells_kernel(wp_dtype), dim=num_systems, inputs=[cell, inv_cell], device=device, )
[docs] def compute_naive_num_shifts( cell: wp.array, cutoff: float, pbc: wp.array, num_shifts: wp.array, shift_range: wp.array, wp_dtype: type, device: str, ) -> None: """Core warp launcher for computing periodic image shifts. Calculates the number and range of periodic boundary shifts required to ensure all atoms within the cutoff distance are found, using pure warp operations. Parameters ---------- cell : wp.array, shape (num_systems, 3, 3), dtype=wp.mat33* Cell matrices defining lattice vectors in Cartesian coordinates. Each 3x3 matrix represents one system's periodic cell. cutoff : float Cutoff distance for neighbor searching in Cartesian units. Must be positive and typically less than half the minimum cell dimension. pbc : wp.array, shape (num_systems, 3), dtype=wp.bool Periodic boundary condition flags for each dimension. True enables periodicity in that direction. num_shifts : wp.array, shape (num_systems,), dtype=wp.int32 OUTPUT: Total number of periodic shifts needed for each system. Updated with calculated shift counts. shift_range : wp.array, shape (num_systems, 3), dtype=wp.vec3i OUTPUT: Maximum shift indices in each dimension for each system. Updated with calculated shift ranges. wp_dtype : type Warp dtype (wp.float32, wp.float64, or wp.float16). device : str Warp device string (e.g., 'cuda:0', 'cpu'). Notes ----- - This is a low-level warp interface. For framework bindings, use torch/jax wrappers. - Output arrays (num_shifts, shift_range) must be pre-allocated by caller. See Also -------- get_compute_naive_num_shifts_kernel : Factory-selected shift-count kernel """ num_systems = cell.shape[0] wp.launch( kernel=get_compute_naive_num_shifts_kernel(wp_dtype), dim=num_systems, inputs=[ cell, wp_dtype(cutoff), pbc, num_shifts, shift_range, ], device=device, )
[docs] def estimate_max_neighbors( cutoff: float, atomic_density: float = 0.2, safety_factor: float | None = None, max_neighbors_lower_bound: int = 16, ) -> int: r"""Estimate maximum neighbors per atom based on volume calculations. Uses atomic density and cutoff volume to estimate a conservative upper bound on the number of neighbors any atom could have. This is a pure Python function with no framework dependencies. Parameters ---------- cutoff : float Maximum distance for considering atoms as neighbors. atomic_density : float, optional Atomic density in atoms per unit volume. Default is 0.2. Increase this for denser or clustered systems whose local density exceeds the bulk average (it scales the estimate linearly). safety_factor : float, optional .. deprecated:: ``safety_factor`` scales the estimate identically to ``atomic_density``; set ``atomic_density`` instead. When given, it is folded into ``atomic_density`` (``atomic_density *= safety_factor``). max_neighbors_lower_bound : int, optional Lower bound on the returned estimate. Default is 16. Raise it for dense or clustered systems where short cutoffs would otherwise underestimate the neighbor count. Returns ------- max_neighbors_estimate : int Conservative estimate of maximum neighbors per atom. Returns 0 for empty systems, and never less than ``max_neighbors_lower_bound`` for a positive cutoff. Notes ----- The estimation uses the formula: .. math:: \text{neighbors} = \text{density} \times V_{\text{sphere}} where the cutoff sphere volume is: .. math:: V_{\text{sphere}} = \frac{4}{3}\pi r^3 The result is floored at ``max_neighbors_lower_bound`` and rounded up to the next multiple of 16 for memory alignment. """ if safety_factor is not None: warnings.warn( "The 'safety_factor' argument to estimate_max_neighbors is " "deprecated; it scales the estimate identically to 'atomic_density'. " "Set 'atomic_density' instead.", DeprecationWarning, stacklevel=2, ) atomic_density = safety_factor * atomic_density if cutoff <= 0: return 0 cutoff_sphere_volume = atomic_density * (4.0 / 3.0) * math.pi * (cutoff**3) # Floor the estimate so short cutoffs keep a safety margin for dense systems. expected_neighbors = max(max_neighbors_lower_bound, cutoff_sphere_volume) # Round up to multiple of 16 for memory alignment and safety max_neighbors_estimate = int(math.ceil(expected_neighbors / 16)) * 16 return max_neighbors_estimate
########################################################################################### ########################### Position Wrapping Kernels #################################### ########################################################################################### def _make_wrap_positions_kernel( wp_dtype: type, *, batched: bool, pbc_aware: bool = False ): """Build a position-wrapping kernel for one dtype and batching mode.""" require_supported_dtype(wp_dtype) vec_dtype, mat_dtype = dtype_info(wp_dtype) BATCHED = wp.constant(bool(batched)) if pbc_aware: @wp.kernel(enable_backward=False, module="unique") def _kernel( positions: wp.array(dtype=vec_dtype), cell: wp.array(dtype=mat_dtype), inv_cell: wp.array(dtype=mat_dtype), pbc: wp.array2d(dtype=wp.bool), batch_idx: wp.array(dtype=wp.int32), positions_wrapped: wp.array(dtype=vec_dtype), per_atom_cell_offsets: wp.array(dtype=wp.vec3i), ) -> None: """Wrap positions into periodic axes and store integer offsets. Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Current Cartesian coordinates. cell : wp.array, shape (num_systems,), dtype=wp.mat33* Cell matrix for each system. inv_cell : wp.array, shape (num_systems,), dtype=wp.mat33* Inverse cell matrix for each system. pbc : wp.array2d, shape (num_systems, 3), dtype=wp.bool Per-axis periodicity flags; wrapping is skipped on non-periodic axes. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. Zero-size sentinel in single-system specializations. positions_wrapped : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Wrapped positions. per_atom_cell_offsets : wp.array, shape (total_atoms,), dtype=wp.vec3i OUTPUT: Integer cell offsets applied per atom. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Thread launch: One thread per atom. - Modifies: ``positions_wrapped`` and ``per_atom_cell_offsets``. ``BATCHED`` is a static specialization. Single-system launchers pass a zero-size ``batch_idx`` sentinel that is not read. See Also -------- get_wrap_positions_kernel : Return the specialized position-wrapping kernel. """ i = wp.tid() isys = wp.int32(0) if BATCHED: isys = batch_idx[i] _cell = cell[isys] _inv_cell = inv_cell[isys] _pbc = pbc[isys] _pos = positions[i] _frac = _pos * _inv_cell _int = wp.vec3i( wp.int32(wp.floor(_frac[0])) if _pbc[0] else wp.int32(0), wp.int32(wp.floor(_frac[1])) if _pbc[1] else wp.int32(0), wp.int32(wp.floor(_frac[2])) if _pbc[2] else wp.int32(0), ) positions_wrapped[i] = _pos - type(_pos)(_int) * _cell per_atom_cell_offsets[i] = _int else: @wp.kernel(enable_backward=False, module="unique") def _kernel( positions: wp.array(dtype=vec_dtype), cell: wp.array(dtype=mat_dtype), inv_cell: wp.array(dtype=mat_dtype), batch_idx: wp.array(dtype=wp.int32), positions_wrapped: wp.array(dtype=vec_dtype), per_atom_cell_offsets: wp.array(dtype=wp.vec3i), ) -> None: """Wrap positions into the primary cell and store integer offsets. Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Current Cartesian coordinates. cell : wp.array, shape (num_systems,), dtype=wp.mat33* Cell matrix for each system. inv_cell : wp.array, shape (num_systems,), dtype=wp.mat33* Inverse cell matrix for each system. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. Zero-size sentinel in single-system specializations. positions_wrapped : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Wrapped positions. per_atom_cell_offsets : wp.array, shape (total_atoms,), dtype=wp.vec3i OUTPUT: Integer cell offsets applied per atom. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Thread launch: One thread per atom. - Modifies: ``positions_wrapped`` and ``per_atom_cell_offsets``. ``BATCHED`` is a static specialization. Single-system launchers pass a zero-size ``batch_idx`` sentinel that is not read. See Also -------- get_wrap_positions_kernel : Return the specialized position-wrapping kernel. """ i = wp.tid() isys = wp.int32(0) if BATCHED: isys = batch_idx[i] _cell = cell[isys] _inv_cell = inv_cell[isys] _pos = positions[i] _frac = _pos * _inv_cell _int = wp.vec3i( wp.int32(wp.floor(_frac[0])), wp.int32(wp.floor(_frac[1])), wp.int32(wp.floor(_frac[2])), ) positions_wrapped[i] = _pos - type(_pos)(_int) * _cell per_atom_cell_offsets[i] = _int base = ( "_wrap_positions_batch_kernel" if batched else "_wrap_positions_single_kernel" ) name = kernel_specialization_name(base, wp_dtype=wp_dtype) return set_fn_doc( set_fn_name(_kernel, name), _append_specialization_doc( _kernel.__doc__, dtype=wp_dtype, entries=( ("batched", bool(batched)), ("pbc_aware", bool(pbc_aware)), ), ), ) @lru_cache(maxsize=None) def get_wrap_positions_kernel( wp_dtype: type, *, batched: bool = False, pbc_aware: bool = False ) -> wp.Kernel: """Return the specialized position-wrapping kernel. Parameters ---------- wp_dtype : type Warp scalar dtype (wp.float32, wp.float64, or wp.float16). batched : bool, optional Whether to build the batched kernel variant. pbc_aware : bool, optional If ``False``, build the existing fold-all-axes kernel signature. If ``True``, build the variant that accepts ``pbc`` and skips wrapping on non-periodic axes. Returns ------- wp.Kernel Specialized position-wrapping kernel. """ return _make_wrap_positions_kernel( wp_dtype, batched=bool(batched), pbc_aware=bool(pbc_aware) ) def _launch_wrap_positions( positions: wp.array, cell: wp.array, inv_cell: wp.array, pbc: wp.array | None, batch_idx: wp.array, positions_wrapped: wp.array, per_atom_cell_offsets: wp.array, wp_dtype: type, device: str, *, batched: bool, ) -> None: """Launch the shared position-wrapping kernel.""" pbc_aware = pbc is not None inputs = [positions, cell, inv_cell] if pbc_aware: inputs.append(pbc) inputs.extend( [ batch_idx if batched else _empty_sentinel(1, wp.int32, device), positions_wrapped, per_atom_cell_offsets, ] ) wp.launch( kernel=get_wrap_positions_kernel( wp_dtype, batched=batched, pbc_aware=pbc_aware ), dim=positions.shape[0], inputs=inputs, device=device, ) def wrap_positions_single( positions: wp.array, cell: wp.array, inv_cell: wp.array, positions_wrapped: wp.array, per_atom_cell_offsets: wp.array, wp_dtype: type, device: str, pbc: wp.array | None = None, ) -> None: """Core warp launcher for wrapping positions into the primary cell (single system). Computes per-atom integer cell offsets and wrapped positions in a single GPU pass. Call this before naive PBC neighbor-list kernels to move the wrapping out of the hot ``ishift`` x ``iatom`` loop. Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Atomic coordinates in Cartesian space. May be unwrapped. cell : wp.array, shape (1,), dtype=wp.mat33* Cell matrix defining lattice vectors. inv_cell : wp.array, shape (1,), dtype=wp.mat33* Pre-computed inverse cell matrix. Must be pre-allocated with the same shape and dtype as *cell*. positions_wrapped : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Wrapped positions. Must be pre-allocated with the same shape and dtype as *positions*. per_atom_cell_offsets : wp.array, shape (total_atoms,), dtype=wp.vec3i OUTPUT: Integer cell offsets per atom. Must be pre-allocated. wp_dtype : type Warp scalar dtype (wp.float32, wp.float64, or wp.float16). device : str Warp device string (e.g., ``'cuda:0'``, ``'cpu'``). pbc : wp.array, shape (1, 3), dtype=wp.bool, optional Per-axis periodicity flags. If omitted, all axes are wrapped. Non-periodic axes are left unwrapped when provided. See Also -------- get_wrap_positions_kernel : Factory-selected wrapping kernel. wrap_positions_batch : Batch variant for multiple systems """ _launch_wrap_positions( positions, cell, inv_cell, pbc, _empty_sentinel(1, wp.int32, device), positions_wrapped, per_atom_cell_offsets, wp_dtype, device, batched=False, ) def wrap_positions_batch( positions: wp.array, cell: wp.array, inv_cell: wp.array, batch_idx: wp.array, positions_wrapped: wp.array, per_atom_cell_offsets: wp.array, wp_dtype: type, device: str, pbc: wp.array | None = None, ) -> None: """Core warp launcher for wrapping positions into the primary cell (batch of systems). Each atom uses the cell matrix of its system (indexed via batch_idx). Computes per-atom integer cell offsets and wrapped positions in a single GPU pass. Call this before batch naive PBC neighbor-list kernels to move the wrapping out of the hot ``ishift`` x ``iatom`` loop. Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Concatenated atomic coordinates for all systems. May be unwrapped. cell : wp.array, shape (num_systems,), dtype=wp.mat33* Cell matrices for each system. inv_cell : wp.array, shape (num_systems,), dtype=wp.mat33* Pre-computed inverse cell matrices. Must be pre-allocated with the same shape and dtype as *cell*. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. positions_wrapped : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Wrapped positions. Must be pre-allocated with the same shape and dtype as *positions*. per_atom_cell_offsets : wp.array, shape (total_atoms,), dtype=wp.vec3i OUTPUT: Integer cell offsets per atom. Must be pre-allocated. wp_dtype : type Warp scalar dtype (wp.float32, wp.float64, or wp.float16). device : str Warp device string (e.g., ``'cuda:0'``, ``'cpu'``). pbc : wp.array, shape (num_systems, 3), dtype=wp.bool, optional Per-system periodicity flags. If omitted, all axes are wrapped. Non-periodic axes are left unwrapped when provided. See Also -------- get_wrap_positions_kernel : Factory-selected wrapping kernel. wrap_positions_single : Single-system variant """ _launch_wrap_positions( positions, cell, inv_cell, pbc, batch_idx, positions_wrapped, per_atom_cell_offsets, wp_dtype, device, batched=True, ) ########################################################################################### ########################### Reference Position Update Kernels ############################ ########################################################################################### def _make_update_ref_positions_kernel(wp_dtype: type, *, batched: bool): """Build a conditional reference-position update kernel.""" require_supported_dtype(wp_dtype, (wp.float32, wp.float64)) vec_dtype, _mat_dtype = dtype_info(wp_dtype, (wp.float32, wp.float64)) BATCHED = wp.constant(bool(batched)) @wp.kernel(enable_backward=False, module="unique") def _kernel( positions: wp.array(dtype=vec_dtype), rebuild_flags: wp.array(dtype=wp.bool), batch_idx: wp.array(dtype=wp.int32), ref_positions: wp.array(dtype=vec_dtype), ) -> None: """Copy current positions into reference positions when rebuilding Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Current Cartesian coordinates. rebuild_flags : wp.array, shape (num_systems,), dtype=wp.bool Rebuild flags controlling which systems update their references. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. Zero-size sentinel in single-system specializations. ref_positions : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Reference coordinates updated for rebuilt systems. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Thread launch: One thread per atom. - Modifies: ``ref_positions`` entries for rebuilt systems. ``BATCHED`` is a static specialization. Single-system launchers pass a zero-size ``batch_idx`` sentinel that is not read. See Also -------- get_update_ref_positions_kernel : Return the specialized reference-position update kernel. """ i = wp.tid() isys = wp.int32(0) if BATCHED: isys = batch_idx[i] if rebuild_flags[isys]: ref_positions[i] = positions[i] base = ( "_update_ref_positions_batch_kernel" if batched else "_update_ref_positions_kernel" ) name = kernel_specialization_name(base, wp_dtype=wp_dtype) return set_fn_doc( set_fn_name(_kernel, name), _append_specialization_doc( _kernel.__doc__, dtype=wp_dtype, entries=(("batched", bool(batched)),), ), ) @lru_cache(maxsize=None) def get_update_ref_positions_kernel( wp_dtype: type, *, batched: bool = False ) -> wp.Kernel: """Return the specialized conditional reference-position update kernel.""" return _make_update_ref_positions_kernel(wp_dtype, batched=bool(batched)) def _launch_update_ref_positions( positions: wp.array, rebuild_flags: wp.array, batch_idx: wp.array, ref_positions: wp.array, wp_dtype: type, device: str, *, batched: bool, ) -> None: """Launch the shared conditional reference-position update kernel.""" wp.launch( kernel=get_update_ref_positions_kernel(wp_dtype, batched=batched), dim=positions.shape[0], inputs=[ positions, rebuild_flags, batch_idx if batched else _empty_sentinel(1, wp.int32, device), ref_positions, ], device=device, ) def update_ref_positions( positions: wp.array, rebuild_flag: wp.array, ref_positions: wp.array, wp_dtype: type, device: str, ) -> None: """Core warp launcher for conditionally updating reference positions (single system). Copies current positions into reference positions only when rebuild_flag[0] is True. No CPU-GPU synchronization required. Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Current atomic coordinates. rebuild_flag : wp.array, shape (1,), dtype=wp.bool Single-system rebuild flag. ref_positions : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Reference positions to update selectively. wp_dtype : type Warp scalar dtype (wp.float32 or wp.float64). device : str Warp device string (e.g., 'cuda:0', 'cpu'). See Also -------- get_update_ref_positions_kernel : Factory-selected update kernel. update_ref_positions_batch : Batch variant """ _launch_update_ref_positions( positions, rebuild_flag, _empty_sentinel(1, wp.int32, device), ref_positions, wp_dtype, device, batched=False, ) def update_ref_positions_batch( positions: wp.array, rebuild_flags: wp.array, batch_idx: wp.array, ref_positions: wp.array, wp_dtype: type, device: str, ) -> None: """Core warp launcher for conditionally updating reference positions (batch). Updates reference positions only for atoms in systems where rebuild_flags is True. No CPU-GPU synchronization required. Parameters ---------- positions : wp.array, shape (total_atoms,), dtype=wp.vec3* Current atomic coordinates for all systems. rebuild_flags : wp.array, shape (num_systems,), dtype=wp.bool Per-system rebuild flags. batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32 System index for each atom. ref_positions : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Reference positions to update selectively. wp_dtype : type Warp scalar dtype (wp.float32 or wp.float64). device : str Warp device string (e.g., 'cuda:0', 'cpu'). See Also -------- get_update_ref_positions_kernel : Factory-selected update kernel. update_ref_positions : Single-system variant """ _launch_update_ref_positions( positions, rebuild_flags, batch_idx, ref_positions, wp_dtype, device, batched=True, ) # ============================================================================= # Fused gather kernels for cell-list pair-centric layout # ============================================================================= def _make_gather_positions_and_shifts_kernel(wp_dtype: type): """Build the fused position/shift gather kernel for ``wp_dtype``. Parameters ---------- wp_dtype : type Warp scalar dtype (``wp.float32`` or ``wp.float64``). Returns ------- wp.Kernel Kernel that writes ``dst_pos[i] = src_pos[perm[i]]`` and ``dst_shifts[i] = src_shifts[perm[i]]``. """ require_supported_dtype(wp_dtype, (wp.float32, wp.float64)) vec_dtype, _mat_dtype = dtype_info(wp_dtype, (wp.float32, wp.float64)) @wp.kernel(enable_backward=False, module="unique") def _kernel( src_pos: wp.array(dtype=vec_dtype), src_shifts: wp.array(dtype=wp.vec3i), perm: wp.array(dtype=wp.int32), dst_pos: wp.array(dtype=vec_dtype), dst_shifts: wp.array(dtype=wp.vec3i), ) -> None: """Gather positions and shifts under one permutation Parameters ---------- src_pos : wp.array, shape (total_atoms,), dtype=wp.vec3* Source positions in original ordering. src_shifts : wp.array, shape (total_atoms,), dtype=wp.vec3i Source periodic shifts in original ordering. perm : wp.array, shape (total_atoms,), dtype=wp.int32 Permutation mapping destination slots to source atom indices. dst_pos : wp.array, shape (total_atoms,), dtype=wp.vec3* OUTPUT: Gathered positions. dst_shifts : wp.array, shape (total_atoms,), dtype=wp.vec3i OUTPUT: Gathered periodic shifts. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Thread launch: One thread per destination slot. - Modifies: ``dst_pos`` and ``dst_shifts``. See Also -------- get_gather_positions_and_shifts_kernel : Return the specialized fused gather kernel. """ i = wp.tid() idx = perm[i] dst_pos[i] = src_pos[idx] dst_shifts[i] = src_shifts[idx] name = kernel_specialization_name("_gather_positions_and_shifts", wp_dtype=wp_dtype) return set_fn_doc( set_fn_name(_kernel, name), _append_specialization_doc( _kernel.__doc__, dtype=wp_dtype, entries=(("operation", "gather_positions_and_shifts"),), ), ) @lru_cache(maxsize=None) def get_gather_positions_and_shifts_kernel(wp_dtype: type) -> wp.Kernel: """Return the specialized fused position/shift gather kernel.""" return _make_gather_positions_and_shifts_kernel(wp_dtype) # ============================================================================= # Neighbor matrix tail-fill kernel + launcher (used by cluster_tile + cell_list) # ============================================================================= FILL_TAIL_BLOCK_DIM = 128 def _make_fill_neighbor_matrix_tail_kernel(block_dim: int): """Build a tiled kernel that fills unused neighbor-matrix columns. Parameters ---------- block_dim : int Static tile width used by ``wp.tile_arange`` and ``wp.launch_tiled``. Returns ------- wp.Kernel Tail-fill kernel specialized to ``block_dim``. """ block_dim = int(block_dim) if block_dim <= 0: raise ValueError("block_dim must be positive") block_dim_const = wp.constant(block_dim) @wp.kernel(enable_backward=False, module=f"tail_fill_block_{block_dim}") def _kernel( num_neighbors: wp.array(dtype=wp.int32), natom: wp.int32, max_neighbors: wp.int32, fill_value: wp.int32, neighbor_matrix: wp.array2d(dtype=wp.int32), ) -> None: """Fill unused neighbor-matrix columns with ``fill_value`` Parameters ---------- num_neighbors : wp.array, shape (natom,), dtype=wp.int32 Active-slot count for each atom row. natom : wp.int32 Number of atom rows to process. max_neighbors : wp.int32 Number of columns in ``neighbor_matrix``. fill_value : wp.int32 Value written to unused columns. neighbor_matrix : wp.array, shape (natom, max_neighbors), dtype=wp.int32 OUTPUT: Neighbor matrix whose inactive tail columns are filled. Returns ------- None This function modifies the input arrays in-place. Notes ----- - Thread launch: Tiled launch with one tile per atom row. - Modifies: Unused columns in ``neighbor_matrix``. ``block_dim`` is a static specialization used by ``wp.tile_arange``. See Also -------- fill_neighbor_matrix_tail : Launch the specialized neighbor-matrix tail fill kernel. """ row = wp.tid() if row >= natom: return nn = num_neighbors[row] if nn >= max_neighbors: return lane_tile = wp.tile_arange(block_dim_const, dtype=wp.int32) lane = wp.untile(lane_tile) k = nn + lane while k < max_neighbors: neighbor_matrix[row, k] = fill_value k += block_dim_const name = kernel_specialization_name( "_fill_neighbor_matrix_tail", features=(f"block_{block_dim}",), ) return set_fn_doc( set_fn_name(_kernel, name), _append_specialization_doc( _kernel.__doc__, entries=( ("operation", "fill_neighbor_matrix_tail"), ("block_dim", block_dim), ), ), ) @lru_cache(maxsize=None) def _get_fill_neighbor_matrix_tail_kernel(block_dim: int): """Return the cached tail-fill kernel for ``block_dim``.""" return _make_fill_neighbor_matrix_tail_kernel(int(block_dim)) def fill_neighbor_matrix_tail( num_neighbors: wp.array, natom: int, max_neighbors: int, fill_value: int, neighbor_matrix: wp.array, device: str, block_dim: int = FILL_TAIL_BLOCK_DIM, ) -> None: """Core warp launcher for coalesced tail-fill of the neighbor matrix. Writes ``fill_value`` into every column of ``neighbor_matrix`` that lies past the active-slot range ``[0, num_neighbors[i])``. Pairs with always-write neighbor-matrix builders (e.g. :func:`nvalchemiops.neighbors.cluster_tile.query_cluster_tile`, pair-centric cell-list queries) so callers can skip the per-step ``neighbor_matrix.fill_(fill_value)`` prefill. Parameters ---------- num_neighbors : wp.array, shape (natom,), dtype=wp.int32 Per-atom active-slot counts. natom : int Number of atoms. max_neighbors : int Column count of ``neighbor_matrix``. fill_value : int Value written into unused columns. neighbor_matrix : wp.array, shape (natom, max_neighbors), dtype=wp.int32 OUTPUT: tail columns filled with ``fill_value``. device : str Warp device string (e.g. ``"cuda:0"``). block_dim : int Static tile width for the specialized tail-fill kernel. Returns ------- None Modifies ``neighbor_matrix`` in-place; see :func:`_make_fill_neighbor_matrix_tail_kernel`. Notes ----- - This is a low-level warp interface. Framework bindings should call it through :mod:`nvalchemiops.torch.neighbors` / :mod:`nvalchemiops.jax.neighbors`. See Also -------- _make_fill_neighbor_matrix_tail_kernel : Factory for the fill kernel. """ block_dim = int(block_dim) wp.launch_tiled( kernel=_get_fill_neighbor_matrix_tail_kernel(block_dim), dim=[int(natom)], inputs=[ num_neighbors, int(natom), int(max_neighbors), int(fill_value), neighbor_matrix, ], block_dim=block_dim, device=device, )