# 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.
"""Warp-facing launchers for cell-list neighbor lists."""
from __future__ import annotations
import warp as wp
from nvalchemiops.neighbors.cell_list.dispatch import (
PAIR_CENTRIC_MAX_LINEAR_LAUNCH,
_pair_centric_coarsened_launch_size,
_pair_centric_logical_block_count,
_pair_centric_work_per_cuda_block,
compute_batch_pair_centric_n_outer,
is_pair_centric_parallelism_sufficient,
pair_centric_launch_size,
select_batch_cell_list_strategy,
select_cell_list_strategy,
)
from nvalchemiops.neighbors.cell_list.kernels import (
_build_cell_to_system_map,
_compute_cells_per_system,
_fill_target_row_lookup,
_reset_target_row_lookup,
get_build_cell_list_kernel,
get_cell_list_cells_per_system_kernel,
get_cell_list_gather_kernel,
get_query_cell_list_kernel,
)
from nvalchemiops.neighbors.neighbor_utils import (
dtype_info,
get_gather_positions_and_shifts_kernel,
selective_zero_num_neighbors,
)
from nvalchemiops.neighbors.neighbor_utils import empty_sentinel as _empty_sentinel
from nvalchemiops.neighbors.output_args import (
_prepare_pair_output_args,
)
__all__ = [
"PAIR_CENTRIC_MAX_LINEAR_LAUNCH",
"batch_build_cell_list",
"batch_query_cell_list",
"batch_query_cell_list_pair_centric_sorted",
"build_cell_list",
"compute_batch_pair_centric_n_outer",
"is_pair_centric_parallelism_sufficient",
"pair_centric_launch_size",
"get_cell_list_cells_per_system_kernel",
"get_build_cell_list_kernel",
"get_cell_list_gather_kernel",
"get_query_cell_list_kernel",
"query_cell_list",
"query_cell_list_atom_centric_sorted",
"query_cell_list_pair_centric_sorted",
"select_batch_cell_list_strategy",
"select_cell_list_strategy",
]
# Inert ``max_radius`` placeholder for the single-system pair-centric launch
# (only the batched kernel reads ``max_radius``). Hoisted to module scope so
# the launch reuses one host-side value instead of rebuilding it per call.
_ZERO_RADIUS = wp.vec3i(0, 0, 0)
def _pair_centric_launch_plan(
total_cells: int,
n_outer: int,
block_dim: int,
*,
max_launch_size: int = PAIR_CENTRIC_MAX_LINEAR_LAUNCH,
) -> tuple[int, int, int, bool]:
"""Return coarsened pair-centric launch metadata.
Returns
-------
tuple[int, int, int, bool]
``(logical_blocks, work_per_cuda_block, launch_dim, coarsened)``.
"""
logical_blocks = _pair_centric_logical_block_count(total_cells, n_outer)
work_per_cuda_block = _pair_centric_work_per_cuda_block(
total_cells,
n_outer,
block_dim,
max_launch_size=max_launch_size,
)
launch_dim = _pair_centric_coarsened_launch_size(
total_cells,
n_outer,
block_dim,
max_launch_size=max_launch_size,
)
coarsened = work_per_cuda_block > 1
return logical_blocks, work_per_cuda_block, launch_dim, coarsened
def _prepare_target_row_lookup(
target_indices: wp.array | None,
target_row_lookup: wp.array | None,
total_atoms: int,
device: str,
) -> wp.array:
"""Return atom-id to compact target-row lookup for pair-centric kernels.
When ``target_indices`` is provided and ``target_row_lookup`` is omitted,
this helper allocates a transient ``(total_atoms,)`` int32 scratch array.
CUDA graph/capture callers should pass caller-owned scratch explicitly.
"""
if target_indices is None:
return _empty_sentinel(1, wp.int32, device)
lookup = target_row_lookup
if lookup is None:
lookup = wp.empty((int(total_atoms),), dtype=wp.int32, device=device)
wp.launch(
_reset_target_row_lookup,
dim=int(total_atoms),
inputs=[lookup],
device=device,
)
wp.launch(
_fill_target_row_lookup,
dim=int(target_indices.shape[0]),
inputs=[target_indices, lookup],
device=device,
)
return lookup
[docs]
def build_cell_list(
positions: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
cells_per_dimension: wp.array,
atom_periodic_shifts: wp.array,
atom_to_cell_mapping: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
wp_dtype: type,
device: str,
min_cells_per_dimension: int = 4,
) -> None:
"""Core warp launcher for building spatial cell list.
Constructs a spatial decomposition data structure for efficient neighbor searching
using pure warp operations. This function launches warp kernels to organize atoms
into spatial cells.
Parameters
----------
positions : wp.array, shape (total_atoms, 3), dtype=wp.vec3*
Atomic coordinates in Cartesian space.
cell : wp.array, shape (1, 3, 3), dtype=wp.mat33*
Unit cell matrix defining the simulation box.
pbc : wp.array, shape (3,), dtype=wp.bool
Periodic boundary condition flags for x, y, z directions.
cutoff : float
Maximum distance for neighbor search.
cells_per_dimension : wp.array, shape (3,), dtype=wp.int32
OUTPUT: Number of cells created in x, y, z directions.
atom_periodic_shifts : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
OUTPUT: Periodic boundary crossings for each atom.
atom_to_cell_mapping : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
OUTPUT: 3D cell coordinates assigned to each atom.
atoms_per_cell_count : wp.array, shape (max_total_cells,), dtype=wp.int32
OUTPUT: Number of atoms in each cell. Must be zeroed by caller before first use.
cell_atom_start_indices : wp.array, shape (max_total_cells,), dtype=wp.int32
OUTPUT: Array for cell start offsets. Caller provides pre-allocated
and zeroed array.
cell_atom_list : wp.array, shape (total_atoms,), dtype=wp.int32
OUTPUT: Flattened list of atom indices organized by cell.
wp_dtype : type
Warp dtype (``wp.float32`` or ``wp.float64``).
device : str
Warp device string (e.g., 'cuda:0', 'cpu').
min_cells_per_dimension : int, default 4
Lower bound for the per-axis cell count. Pass 1 for the legacy grid rule.
Notes
-----
- This is a low-level Warp interface. The caller must ensure
``atoms_per_cell_count`` is zeroed before calling.
- This function handles the cumsum internally using ``wp.utils.array_scan``.
- For framework bindings, use the torch/jax wrappers instead.
See Also
--------
query_cell_list : Query cell list to build neighbor matrix (call after this)
"""
total_atoms = positions.shape[0]
max_total_cells = atoms_per_cell_count.shape[0]
wp_cutoff = wp_dtype(cutoff)
wp.launch(
get_build_cell_list_kernel(
"construct_bin_size",
wp_dtype,
min_cells_per_dimension=int(min_cells_per_dimension),
),
dim=1,
device=device,
inputs=(
cell,
pbc,
_empty_sentinel(2, wp.bool, device),
cells_per_dimension,
_empty_sentinel(1, wp.vec3i, device),
wp_cutoff,
max_total_cells,
),
)
wp.launch(
get_build_cell_list_kernel("count_atoms", wp_dtype),
dim=total_atoms,
inputs=[
positions,
cell,
pbc,
_empty_sentinel(2, wp.bool, device),
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
_empty_sentinel(1, wp.vec3i, device),
_empty_sentinel(1, wp.int32, device),
atoms_per_cell_count,
atom_periodic_shifts,
],
device=device,
)
wp.utils.array_scan(atoms_per_cell_count, cell_atom_start_indices, inclusive=False)
atoms_per_cell_count.zero_()
wp.launch(
get_build_cell_list_kernel("bin_atoms", wp_dtype),
dim=total_atoms,
inputs=[
positions,
cell,
pbc,
_empty_sentinel(2, wp.bool, device),
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
_empty_sentinel(1, wp.vec3i, device),
_empty_sentinel(1, wp.int32, device),
atom_to_cell_mapping,
atoms_per_cell_count,
cell_atom_start_indices,
cell_atom_list,
],
device=device,
)
def query_cell_list_atom_centric_sorted(
sorted_positions: wp.array,
sorted_atom_periodic_shifts: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
cells_per_dimension: wp.array,
neighbor_search_radius: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
atom_to_cell_mapping: wp.array,
neighbor_matrix: wp.array,
neighbor_matrix_shifts: wp.array,
num_neighbors: wp.array,
rebuild_flags: wp.array,
wp_dtype: type,
device: str,
half_fill: bool = True,
*,
positions: wp.array | None = None,
atom_periodic_shifts: wp.array | None = None,
target_indices: wp.array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: wp.array | None = None,
neighbor_vectors: wp.array | None = None,
neighbor_distances: wp.array | None = None,
pair_energies: wp.array | None = None,
pair_forces: wp.array | None = None,
atom_centric_path: str = "sorted",
selective: bool = True,
) -> None:
"""Atom-centric query with sorted-position reads.
The caller pre-gathers ``sorted_positions`` and
``sorted_atom_periodic_shifts`` via ``gather_fused``. Output writes
target the public ``neighbor_matrix[atom_idx, n]`` indexed by the
original atom index, so downstream code is unaware of the reordering.
``half_fill=True`` iterates the half-shell + ``j > i`` self filter;
``half_fill=False`` iterates the full shell + ``j != i`` self filter.
The local-counter (per-thread register) is valid in both modes
because each thread is the unique writer to its own row.
``rebuild_flags`` is a caller-allocated 1-element ``wp.bool`` array for
selective rebuilds. Non-selective callers pass a sentinel array and set
``selective=False`` so the kernel specialization does not read it.
Parameters
----------
sorted_positions : wp.array, shape (num_atoms,), dtype=wp.vec3*
Cell-contiguous reordered positions.
sorted_atom_periodic_shifts : wp.array, shape (num_atoms,), dtype=wp.vec3i
Cell-contiguous reordered shifts.
cell : wp.array, shape (1, 3, 3), dtype=wp.mat33*
Simulation cell matrix.
pbc : wp.array, shape (3,), dtype=wp.bool
Per-axis periodic-boundary flags.
cutoff : float
Neighbor search cutoff.
cells_per_dimension : wp.array, shape (3,), dtype=wp.int32
Cell grid shape.
neighbor_search_radius : wp.array, shape (3,), dtype=wp.int32
Per-axis search radius.
atoms_per_cell_count : wp.array, shape (num_cells,), dtype=wp.int32
Atoms per cell.
cell_atom_start_indices : wp.array, shape (num_cells,), dtype=wp.int32
CSR cell-start indices.
cell_atom_list : wp.array, shape (num_atoms,), dtype=wp.int32
CSR atom indices.
atom_to_cell_mapping : wp.array, shape (num_atoms,), dtype=wp.vec3i
Atom -> cell coordinate map.
neighbor_matrix : wp.array, shape (num_atoms, max_neighbors), dtype=wp.int32
OUTPUT: per-atom neighbor indices.
neighbor_matrix_shifts : wp.array, shape (num_atoms, max_neighbors), dtype=wp.vec3i
OUTPUT: per-pair lattice shift.
num_neighbors : wp.array, shape (num_atoms,), dtype=wp.int32
OUTPUT: per-atom neighbor count.
rebuild_flags : wp.array, shape (1,), dtype=wp.bool
Selective-rebuild flag.
wp_dtype : type
Warp scalar dtype.
device : str
Warp device string.
half_fill : bool, default ``True``
Half-shell + ``j > i`` filter when True.
positions, atom_periodic_shifts : wp.array, optional
Unsorted positions / shifts. Required when ``target_indices`` is
provided so the central-atom read does not need to invert the
sorted layout.
target_indices : wp.array, shape (num_targets,), dtype=wp.int32, optional
Restrict source atoms to a subset. Switches the kernel to a
per-row iteration over ``target_indices`` and consults ``positions``
/ ``atom_periodic_shifts`` for central-atom reads.
return_vectors, return_distances : bool, default ``False``
Write per-pair displacement vectors / distances to
``neighbor_vectors`` / ``neighbor_distances``.
pair_fn : callable, optional
Module-scope ``@wp.func`` of signature
``(r_ij, distance, pair_params, i, j) -> (energy, force)``. When
provided, the launcher requires ``pair_params``,
``pair_energies`` and ``pair_forces``.
pair_params : wp.array, shape (num_atoms, num_parameters), optional
Per-atom pair-function parameters. Dtype must match positions.
neighbor_vectors : wp.array, shape (num_atoms, max_neighbors), dtype=wp.vec3*, optional
OUTPUT buffer for per-pair displacements (only with ``return_vectors``).
neighbor_distances : wp.array, shape (num_atoms, max_neighbors), dtype=wp_dtype, optional
OUTPUT buffer for per-pair distances (only with ``return_distances``).
pair_energies : wp.array, shape (num_atoms, max_neighbors), dtype=wp_dtype, optional
OUTPUT buffer for per-pair energies (only with ``pair_fn``).
pair_forces : wp.array, shape (num_atoms, max_neighbors), dtype=wp.vec3*, optional
OUTPUT buffer for per-pair forces (only with ``pair_fn``).
selective : bool, default ``True``
Whether ``rebuild_flags`` controls kernel execution.
Notes
-----
The same atom-centric kernel factory handles the default path, compact
``target_indices`` rows, optional vector/distance buffers, and ``pair_fn``
slot outputs.
"""
partial = target_indices is not None
if atom_centric_path not in {"direct", "sorted"}:
raise ValueError(
f"atom_centric_path must be 'direct' | 'sorted', got {atom_centric_path!r}",
)
if (partial or atom_centric_path == "direct") and (
positions is None or atom_periodic_shifts is None
):
raise ValueError(
"positions and atom_periodic_shifts are required for the "
f"atom-centric {atom_centric_path!r} path",
)
positions_arg = positions if positions is not None else sorted_positions
atom_periodic_shifts_arg = (
atom_periodic_shifts
if atom_periodic_shifts is not None
else sorted_atom_periodic_shifts
)
target_indices_arg = (
target_indices
if target_indices is not None
else _empty_sentinel(1, wp.int32, device)
)
(
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
) = _prepare_pair_output_args(
wp_dtype,
device,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
)
kernel = get_query_cell_list_kernel(
wp_dtype,
strategy="atom_centric",
batched=False,
selective=bool(selective),
partial=partial,
half_fill=bool(half_fill),
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
atom_centric_path=atom_centric_path,
)
if partial:
dim = int(target_indices.shape[0])
elif atom_centric_path == "direct":
dim = int(positions_arg.shape[0])
else:
dim = int(sorted_positions.shape[0])
wp.launch(
kernel,
dim=dim,
inputs=[
positions_arg,
atom_periodic_shifts_arg,
sorted_positions,
sorted_atom_periodic_shifts,
cell,
pbc,
_empty_sentinel(2, wp.bool, device),
_empty_sentinel(1, wp.int32, device),
wp_dtype(cutoff),
cells_per_dimension,
_empty_sentinel(1, wp.vec3i, device),
neighbor_search_radius,
_empty_sentinel(1, wp.vec3i, device),
atom_to_cell_mapping,
atoms_per_cell_count,
cell_atom_start_indices,
cell_atom_list,
_empty_sentinel(1, wp.int32, device),
target_indices_arg,
neighbor_matrix,
neighbor_matrix_shifts,
num_neighbors,
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
rebuild_flags,
],
device=device,
)
def query_cell_list_pair_centric_sorted(
sorted_positions: wp.array,
sorted_atom_periodic_shifts: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
cells_per_dimension: wp.array,
neighbor_search_radius: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
neighbor_matrix: wp.array,
neighbor_matrix_shifts: wp.array,
num_neighbors: wp.array,
rebuild_flags: wp.array,
wp_dtype: type,
device: str,
n_outer: int,
block_dim: int = 64,
half_fill: bool = True,
*,
target_indices: wp.array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: wp.array | None = None,
neighbor_vectors: wp.array | None = None,
neighbor_distances: wp.array | None = None,
pair_energies: wp.array | None = None,
pair_forces: wp.array | None = None,
target_row_lookup: wp.array | None = None,
selective: bool = True,
max_launch_size: int = PAIR_CENTRIC_MAX_LINEAR_LAUNCH,
) -> None:
"""Pair-centric query: one block per (source cell, offset).
One factory-generated kernel handles both same-cell pairs and outer
cell-cell pairs. ``offset_idx == 0`` is the same-cell case; nonzero
offsets are decoded on-the-fly from ``neighbor_search_radius`` and
``half_fill`` via :func:`_decode_shift_index` /
:func:`_decode_full_shift_index`.
The kernel uses per-emit ``wp.atomic_add(num_neighbors, atom_i, 1)``
because multiple blocks (different outer offsets) may target the
same source atom.
Pair-set output is identical to the atom-centric kernel; per-row
ordering inside ``neighbor_matrix`` is non-deterministic across
builds.
Oversized launches transparently coarsen multiple logical
``(source_cell, offset)`` blocks into each CUDA block while keeping
the same strategy name.
Parameters
----------
sorted_positions, sorted_atom_periodic_shifts : wp.array
Pre-gathered sorted-by-cell views (output of ``gather_fused``).
neighbor_search_radius : wp.array(dtype=wp.int32), shape (3,)
Per-axis radius ``(Rx, Ry, Rz)``. The kernel decodes each block's
outer-offset index into a ``(dx, dy, dz)`` vector on-the-fly via
the shared shift-index decoders.
n_outer : int
Number of non-self outer cell offsets - must match
``compute_batch_pair_centric_n_outer(R, half_fill)``. Caller computes
this once per geometry change (the only host-side dependency on
the per-axis radius).
half_fill : bool, default True
Half-shell + ``j > i`` self filter when True; full shell + ``j != i``
self filter when False. Same semantics as the atom-centric kernel.
block_dim : int, default 64
Threads per CUDA block. Should bracket typical
atoms-per-cell counts; cells with more atoms see lanes
stride-loop.
max_launch_size : int, default PAIR_CENTRIC_MAX_LINEAR_LAUNCH
Internal/test hook for the safe one-dimensional Warp launch limit.
rebuild_flags : wp.array(dtype=wp.bool), shape (1,)
Caller-allocated GPU-resident flag. ``False`` makes every block
return immediately and outputs are preserved. ``True`` runs the
full build - caller must zero ``num_neighbors`` first so the
per-emit atomic_adds start from 0. Non-selective callers pass a
sentinel array and set ``selective=False``.
Notes
-----
The pair-centric kernel factory supports compact ``target_indices`` rows,
optional vector/distance buffers, and ``pair_fn`` slot outputs directly.
It remains CUDA-only because it maps logical blocks to
``(source_cell, offset)`` work items.
"""
if "cpu" in str(device).lower():
raise ValueError(
"strategy='pair_centric' is not supported on CPU "
"(kernels use CUDA block scheduling). Pass 'atom_centric' instead.",
)
total_atoms = int(sorted_positions.shape[0])
total_cells = int(atoms_per_cell_count.shape[0])
block_dim_int = int(block_dim)
n_offsets_int = int(n_outer) + 1
logical_blocks, work_per_cuda_block, launch_dim, coarsened = (
_pair_centric_launch_plan(
total_cells,
int(n_outer),
block_dim_int,
max_launch_size=max_launch_size,
)
)
partial = target_indices is not None
target_row_lookup_arg = _prepare_target_row_lookup(
target_indices,
target_row_lookup,
total_atoms,
device,
)
(
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
) = _prepare_pair_output_args(
wp_dtype,
device,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
)
kernel = get_query_cell_list_kernel(
wp_dtype,
strategy="pair_centric",
batched=False,
selective=bool(selective),
partial=partial,
half_fill=bool(half_fill),
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
coarsened=coarsened,
)
wp.launch(
kernel,
dim=launch_dim,
block_dim=block_dim_int,
inputs=[
sorted_positions,
sorted_atom_periodic_shifts,
cell,
pbc,
_empty_sentinel(2, wp.bool, device),
wp_dtype(cutoff),
cells_per_dimension,
_empty_sentinel(1, wp.vec3i, device),
neighbor_search_radius,
_empty_sentinel(1, wp.vec3i, device),
atoms_per_cell_count,
cell_atom_start_indices,
cell_atom_list,
_empty_sentinel(1, wp.int32, device),
_empty_sentinel(1, wp.int32, device),
target_row_lookup_arg,
neighbor_matrix,
neighbor_matrix_shifts,
num_neighbors,
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
block_dim_int,
total_cells,
n_offsets_int,
logical_blocks,
work_per_cuda_block,
_ZERO_RADIUS,
rebuild_flags,
],
device=device,
)
[docs]
def query_cell_list(
positions: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
cells_per_dimension: wp.array,
neighbor_search_radius: wp.array,
atom_periodic_shifts: wp.array,
atom_to_cell_mapping: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
neighbor_matrix: wp.array,
neighbor_matrix_shifts: wp.array,
num_neighbors: wp.array,
wp_dtype: type,
device: str,
half_fill: bool = False,
rebuild_flags: wp.array | None = None,
*,
sorted_positions: wp.array | None = None,
sorted_atom_periodic_shifts: wp.array | None = None,
strategy: str = "atom_centric",
atom_centric_path: str = "auto",
n_outer: int | None = None,
target_indices: wp.array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: wp.array | None = None,
neighbor_vectors: wp.array | None = None,
neighbor_distances: wp.array | None = None,
pair_energies: wp.array | None = None,
pair_forces: wp.array | None = None,
target_row_lookup: wp.array | None = None,
max_launch_size: int = PAIR_CENTRIC_MAX_LINEAR_LAUNCH,
) -> None:
"""Core warp launcher for querying spatial cell list to build neighbor matrix.
Uses pre-built cell list data structures to efficiently find all atom pairs
within the specified cutoff distance using pure warp operations. Output
arrays (``neighbor_matrix``, ``neighbor_matrix_shifts``, ``num_neighbors``)
are caller-allocated. The per-cell-contiguous gather scratch
(``sorted_positions``, ``sorted_atom_periodic_shifts``) and the
1-element ``rebuild_flags`` are optional: when omitted, this launcher
uses the non-selective kernel specialization. Graph/capture callers should
pass caller-owned scratch explicitly to keep allocations out of the
captured region. If ``target_indices`` is used with pair-centric mode
and ``target_row_lookup`` is omitted, this launcher allocates a transient
lookup scratch array.
Parameters
----------
positions : wp.array, shape (total_atoms, 3), dtype=wp.vec3*
Atomic coordinates in Cartesian space.
cell : wp.array, shape (1, 3, 3), dtype=wp.mat33*
Unit cell matrix for periodic boundary coordinate shifts.
pbc : wp.array, shape (3,), dtype=wp.bool
Periodic boundary condition flags.
cutoff : float
Maximum distance for considering atoms as neighbors.
cells_per_dimension : wp.array, shape (3,), dtype=wp.int32
Number of cells in x, y, z directions from build_cell_list.
neighbor_search_radius : wp.array, shape (3,), dtype=wp.int32
Radius of neighboring cells to search in each dimension.
atom_periodic_shifts : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
Periodic boundary crossings for each atom from build_cell_list.
atom_to_cell_mapping : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
3D cell coordinates for each atom from build_cell_list.
atoms_per_cell_count : wp.array, shape (max_total_cells,), dtype=wp.int32
Number of atoms in each cell from build_cell_list.
cell_atom_start_indices : wp.array, shape (max_total_cells,), dtype=wp.int32
Starting index in cell_atom_list for each cell from build_cell_list.
cell_atom_list : wp.array, shape (total_atoms,), dtype=wp.int32
Flattened list of atom indices organized by cell from build_cell_list.
neighbor_matrix : wp.array, shape (total_atoms, max_neighbors), dtype=wp.int32
OUTPUT: Neighbor matrix to be filled with neighbor atom indices.
neighbor_matrix_shifts : wp.array, shape (total_atoms, max_neighbors, 3), dtype=wp.vec3i
OUTPUT: Matrix storing shift vectors for each neighbor relationship.
num_neighbors : wp.array, shape (total_atoms,), dtype=wp.int32
OUTPUT (atomic): per-atom neighbor counts. Accumulated via
``wp.atomic_add``, so non-selective callers must zero it before the
call (selective callers zero it via ``rebuild_flags``).
wp_dtype : type
Warp dtype (``wp.float32`` or ``wp.float64``).
device : str
Warp device string (e.g., 'cuda:0', 'cpu').
sorted_positions : wp.array, shape (total_atoms,), dtype=wp.vec3*, optional
Per-cell-contiguous gather scratch. ``gather_fused`` writes into
it each call. Allocated transiently when omitted; graph/capture
callers should pass caller-owned scratch.
sorted_atom_periodic_shifts : wp.array, shape (total_atoms,), dtype=wp.vec3i, optional
Per-cell-contiguous gather scratch. ``gather_fused`` writes into
it each call. Allocated transiently when omitted; graph/capture
callers should pass caller-owned scratch.
rebuild_flags : wp.array, shape (1,), dtype=wp.bool, optional
1-element flag. ``False`` makes the kernel return immediately.
When omitted, this launcher uses the non-selective kernel
specialization and does not read a rebuild flag.
half_fill : bool, default=False
If True, only store half of the neighbor relationships (i < j).
strategy : {"atom_centric", "pair_centric"}, default "atom_centric"
Selects which of the two sorted fast-path kernels to launch.
Both produce identical pair sets for either ``half_fill`` value;
per-row ordering inside ``neighbor_matrix`` differs.
``"pair_centric"`` requires ``n_outer`` (the host-side count of
non-self outer cell offsets at the per-axis radius - caller
precomputes via :func:`compute_batch_pair_centric_n_outer`). Auto-
strategy selection (sync-free) lives at the torch-wrapper layer where the
sync is already paid; direct-warp callers pick explicitly.
Pair-centric is CUDA-only - CPU callers must use atom-centric.
n_outer : int, optional
Required when ``strategy="pair_centric"``. Number of non-self
outer cell offsets at the per-axis search radius - see
:func:`compute_batch_pair_centric_n_outer` for the closed form.
target_indices : wp.array, shape (num_targets,), dtype=wp.int32, optional
Restrict central rows to a subset of atom indices. Output rows are
compact and follow ``target_indices`` order for both strategies.
return_vectors, return_distances : bool, default ``False``
Write per-pair displacement vectors / distances into
``neighbor_vectors`` / ``neighbor_distances``.
pair_fn : callable, optional
Module-scope ``@wp.func`` of signature
``(r_ij, distance, pair_params, i, j) -> (energy, force)``.
pair_params : wp.array, shape (num_atoms, num_parameters), optional
Per-atom pair-function parameters; required with ``pair_fn``.
neighbor_vectors, neighbor_distances : wp.array, optional
OUTPUT buffers for per-pair displacements / distances.
pair_energies, pair_forces : wp.array, optional
OUTPUT buffers for per-pair energies / forces; required with
``pair_fn``.
Notes
-----
- Output and scratch arrays are caller-owned except for the optional
transient ``target_row_lookup`` allocation described above.
``num_neighbors`` must be zeroed before each pair-centric call
(atomic_add semantics). Shifts output uses the always-write
contract (no prefill required).
- Both strategies support compact ``target_indices`` rows, optional
vector/distance buffers, and ``pair_fn`` slot outputs.
See Also
--------
build_cell_list : Build cell list (call before this)
query_cell_list_atom_centric_sorted : Atom-centric kernel (both half_fill modes)
query_cell_list_pair_centric_sorted : Pair-centric alternative
select_cell_list_strategy : Sync-free (N, cutoff) auto-selection rule
compute_batch_pair_centric_n_outer : Closed-form for ``n_outer``
"""
total_atoms = positions.shape[0]
selective = rebuild_flags is not None
rebuild_flags_arg = (
rebuild_flags
if rebuild_flags is not None
else _empty_sentinel(1, wp.bool, device)
)
cpu_only = "cpu" in str(device).lower()
if strategy == "atom_centric":
chosen = "atom_centric"
elif strategy == "pair_centric":
if cpu_only:
raise ValueError(
"strategy='pair_centric' is not supported on CPU "
"(kernels use CUDA block scheduling). Pass "
"'atom_centric' instead.",
)
if n_outer is None:
raise ValueError(
"strategy='pair_centric' requires n_outer. Compute via "
"compute_batch_pair_centric_n_outer((Rx, Ry, Rz), half_fill).",
)
chosen = "pair_centric"
else:
raise ValueError(
f"strategy must be 'atom_centric' | 'pair_centric', got {strategy!r}",
)
if atom_centric_path == "auto":
atom_centric_path = "direct"
elif atom_centric_path not in {"direct", "sorted"}:
raise ValueError(
"atom_centric_path must be 'auto' | 'direct' | 'sorted', "
f"got {atom_centric_path!r}",
)
needs_sorted = chosen == "pair_centric" or atom_centric_path == "sorted"
_vec_dtype, _ = dtype_info(wp_dtype)
if needs_sorted:
if (sorted_positions is None) != (sorted_atom_periodic_shifts is None):
raise ValueError(
"Pass both sorted_positions and sorted_atom_periodic_shifts, "
"or neither - got a mixed state.",
)
if sorted_positions is None:
sorted_positions = wp.empty(
int(total_atoms), dtype=_vec_dtype, device=device
)
if sorted_atom_periodic_shifts is None:
sorted_atom_periodic_shifts = wp.empty(
int(total_atoms), dtype=wp.vec3i, device=device
)
else:
sorted_positions = _empty_sentinel(1, _vec_dtype, device)
sorted_atom_periodic_shifts = _empty_sentinel(1, wp.vec3i, device)
if needs_sorted:
wp.launch(
get_gather_positions_and_shifts_kernel(wp_dtype),
dim=total_atoms,
inputs=[
positions,
atom_periodic_shifts,
cell_atom_list,
sorted_positions,
sorted_atom_periodic_shifts,
],
device=device,
)
if chosen == "pair_centric":
query_cell_list_pair_centric_sorted(
sorted_positions=sorted_positions,
sorted_atom_periodic_shifts=sorted_atom_periodic_shifts,
cell=cell,
pbc=pbc,
cutoff=float(cutoff),
cells_per_dimension=cells_per_dimension,
neighbor_search_radius=neighbor_search_radius,
atoms_per_cell_count=atoms_per_cell_count,
cell_atom_start_indices=cell_atom_start_indices,
cell_atom_list=cell_atom_list,
neighbor_matrix=neighbor_matrix,
neighbor_matrix_shifts=neighbor_matrix_shifts,
num_neighbors=num_neighbors,
rebuild_flags=rebuild_flags_arg,
wp_dtype=wp_dtype,
device=device,
n_outer=int(n_outer),
block_dim=64,
half_fill=bool(half_fill),
target_indices=target_indices,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
target_row_lookup=target_row_lookup,
selective=selective,
max_launch_size=max_launch_size,
)
else:
query_cell_list_atom_centric_sorted(
sorted_positions=sorted_positions,
sorted_atom_periodic_shifts=sorted_atom_periodic_shifts,
cell=cell,
pbc=pbc,
cutoff=float(cutoff),
cells_per_dimension=cells_per_dimension,
neighbor_search_radius=neighbor_search_radius,
atoms_per_cell_count=atoms_per_cell_count,
cell_atom_start_indices=cell_atom_start_indices,
cell_atom_list=cell_atom_list,
atom_to_cell_mapping=atom_to_cell_mapping,
neighbor_matrix=neighbor_matrix,
neighbor_matrix_shifts=neighbor_matrix_shifts,
num_neighbors=num_neighbors,
rebuild_flags=rebuild_flags_arg,
wp_dtype=wp_dtype,
device=device,
half_fill=bool(half_fill),
positions=positions,
atom_periodic_shifts=atom_periodic_shifts,
target_indices=target_indices,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
atom_centric_path=atom_centric_path,
selective=selective,
)
# ----------------------------------------------------------------------------
# Public launchers - batched path
# ----------------------------------------------------------------------------
[docs]
def batch_build_cell_list(
positions: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
batch_idx: wp.array,
cells_per_dimension: wp.array,
cell_offsets: wp.array,
cells_per_system: wp.array,
atom_periodic_shifts: wp.array,
atom_to_cell_mapping: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
wp_dtype: type,
device: str,
min_cells_per_dimension: int = 4,
) -> None:
"""Core warp launcher for building batch spatial cell lists.
Constructs spatial decomposition data structures for multiple systems using
pure warp operations. This function launches warp kernels to organize atoms
into spatial cells across all systems in the batch.
Parameters
----------
positions : wp.array, shape (total_atoms, 3), dtype=wp.vec3*
Concatenated atomic coordinates for all systems in the batch.
cell : wp.array, shape (num_systems, 3, 3), dtype=wp.mat33*
Unit cell matrices for each system in the batch.
pbc : wp.array, shape (num_systems, 3), dtype=wp.bool
Periodic boundary condition flags for each system and dimension.
cutoff : float
Neighbor search cutoff distance.
batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32
System index for each atom.
cells_per_dimension : wp.array, shape (num_systems, 3), dtype=wp.vec3i
OUTPUT: Number of cells in x, y, z directions for each system.
cell_offsets : wp.array, shape (num_systems,), dtype=wp.int32
OUTPUT: Starting index in global cell arrays for each system.
Computed internally via exclusive scan of cells_per_dimension products.
cells_per_system : wp.array, shape (num_systems,), dtype=wp.int32
SCRATCH: Temporary buffer for total cells per system.
Used as input to exclusive scan for computing cell_offsets.
Must be pre-allocated by caller.
atom_periodic_shifts : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
OUTPUT: Periodic boundary crossings for each atom.
atom_to_cell_mapping : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
OUTPUT: 3D cell coordinates assigned to each atom.
atoms_per_cell_count : wp.array, shape (max_total_cells,), dtype=wp.int32
OUTPUT: Number of atoms in each cell. Must be zeroed by caller before first use.
cell_atom_start_indices : wp.array, shape (max_total_cells,), dtype=wp.int32
OUTPUT: Starting index in cell_atom_list for each cell's atoms.
cell_atom_list : wp.array, shape (total_atoms,), dtype=wp.int32
OUTPUT: Flattened list of atom indices organized by cell.
wp_dtype : type
Warp dtype (``wp.float32`` or ``wp.float64``).
device : str
Warp device string (e.g., 'cuda:0', 'cpu').
min_cells_per_dimension : int, default 4
Lower bound for the per-axis cell count. Pass 1 for the legacy grid rule.
Notes
-----
- This is a low-level warp interface. Caller must ensure atoms_per_cell_count is zeroed.
- cell_offsets is computed internally after cells_per_dimension is determined.
- This function handles the internal cumsum for cell_atom_start_indices using wp.utils.array_scan.
- For framework bindings, use the torch/jax wrappers instead.
See Also
--------
batch_query_cell_list : Query cell list to build neighbor matrix (call after this)
"""
total_atoms = positions.shape[0]
num_systems = cell.shape[0]
max_total_cells = atoms_per_cell_count.shape[0]
wp_cutoff = wp_dtype(cutoff)
wp.launch(
get_build_cell_list_kernel(
"construct_bin_size",
wp_dtype,
batched=True,
min_cells_per_dimension=int(min_cells_per_dimension),
),
dim=num_systems,
device=device,
inputs=(
cell,
_empty_sentinel(1, wp.bool, device),
pbc,
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
wp_cutoff,
max_total_cells,
),
)
wp.launch(
_compute_cells_per_system,
dim=num_systems,
device=device,
inputs=(cells_per_dimension, cells_per_system),
)
wp.utils.array_scan(cells_per_system, cell_offsets, inclusive=False)
wp.launch(
get_build_cell_list_kernel("count_atoms", wp_dtype, batched=True),
dim=total_atoms,
inputs=(
positions,
cell,
_empty_sentinel(1, wp.bool, device),
pbc,
batch_idx,
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
cell_offsets,
atoms_per_cell_count,
atom_periodic_shifts,
),
device=device,
)
wp.utils.array_scan(atoms_per_cell_count, cell_atom_start_indices, inclusive=False)
atoms_per_cell_count.zero_()
wp.launch(
get_build_cell_list_kernel("bin_atoms", wp_dtype, batched=True),
dim=total_atoms,
inputs=(
positions,
cell,
_empty_sentinel(1, wp.bool, device),
pbc,
batch_idx,
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
cell_offsets,
atom_to_cell_mapping,
atoms_per_cell_count,
cell_atom_start_indices,
cell_atom_list,
),
device=device,
)
def batch_query_cell_list_pair_centric_sorted(
positions: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
cells_per_dimension: wp.array,
neighbor_search_radius: wp.array,
cell_offsets: wp.array,
cells_per_system: wp.array,
atom_periodic_shifts: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
sorted_positions: wp.array,
sorted_atom_periodic_shifts: wp.array,
cell_to_system: wp.array,
neighbor_matrix: wp.array,
neighbor_matrix_shifts: wp.array,
num_neighbors: wp.array,
wp_dtype: type,
device: str,
total_cells: int,
n_outer: int,
R_max: tuple[int, int, int],
half_fill: bool = True,
block_dim: int = 64,
*,
target_indices: wp.array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: wp.array | None = None,
neighbor_vectors: wp.array | None = None,
neighbor_distances: wp.array | None = None,
pair_energies: wp.array | None = None,
pair_forces: wp.array | None = None,
rebuild_flags: wp.array | None = None,
target_row_lookup: wp.array | None = None,
max_launch_size: int = PAIR_CENTRIC_MAX_LINEAR_LAUNCH,
) -> None:
"""Core warp launcher for the pair-centric batched cell-list query.
Launches the pair-centric kernel over ``total_cells x (n_outer + 1)``
logical blocks of ``block_dim`` threads, coarsening multiple logical
blocks per CUDA block when the uncoarsened launch would exceed the
safe one-dimensional Warp launch limit. ``offset_idx == 0`` is the
self-cell entry (within-cell pairs, with the appropriate filter
for ``half_fill``); ``offset_idx > 0`` are the outer-shell offsets
decoded on-the-fly from ``R_max``. Per-system out-of-range offsets
early-return.
Per-emit ``wp.atomic_add(num_neighbors, atom_i, 1)`` because multiple
blocks may write to the same atom's row; caller must pre-zero
``num_neighbors``.
Parameters
----------
positions : wp.array, shape (total_atoms,), dtype=wp.vec3*
Concatenated atomic coordinates for all systems.
cell : wp.array, shape (num_systems, 3, 3), dtype=wp.mat33*
Unit cell matrices for each system.
pbc : wp.array, shape (num_systems, 3), dtype=wp.bool
Periodic boundary condition flags.
cutoff : float
Neighbor search cutoff distance.
cells_per_dimension : wp.array, shape (num_systems, 3), dtype=wp.vec3i
Cells per axis for each system. From :func:`batch_build_cell_list`.
neighbor_search_radius : wp.array, shape (num_systems, 3), dtype=wp.vec3i
Per-system search radius. From :func:`batch_build_cell_list`.
cell_offsets : wp.array, shape (num_systems,), dtype=wp.int32
Starting global cell index per system. From :func:`batch_build_cell_list`.
cells_per_system : wp.array, shape (num_systems,), dtype=wp.int32
Number of cells per system. From :func:`batch_build_cell_list`.
atom_periodic_shifts : wp.array, shape (total_atoms,), dtype=wp.vec3i
Per-atom PBC shift vectors. From :func:`batch_build_cell_list`.
atoms_per_cell_count : wp.array, shape (total_cells,), dtype=wp.int32
Atoms per cell. From :func:`batch_build_cell_list`.
cell_atom_start_indices : wp.array, shape (total_cells,), dtype=wp.int32
Cell start indices into ``cell_atom_list``. From :func:`batch_build_cell_list`.
cell_atom_list : wp.array, shape (total_atoms,), dtype=wp.int32
Cell-organized atom indices. From :func:`batch_build_cell_list`.
sorted_positions : wp.array, shape (total_atoms,), dtype=wp.vec3*
OUTPUT (scratch): cell-contiguous reordered positions. Caller
allocates; ``_gather_positions_by_cell`` overwrites each call.
sorted_atom_periodic_shifts : wp.array, shape (total_atoms,), dtype=wp.vec3i
OUTPUT (scratch): cell-contiguous reordered shifts.
cell_to_system : wp.array, shape (total_cells,), dtype=wp.int32
OUTPUT (scratch): global-cell -> system map. Caller allocates;
``_build_cell_to_system_map`` overwrites each call.
neighbor_matrix : wp.array, shape (total_atoms, max_neighbors), dtype=wp.int32
OUTPUT: neighbor indices.
neighbor_matrix_shifts : wp.array, shape (total_atoms, max_neighbors, 3), dtype=wp.vec3i
OUTPUT: per-pair PBC shift vectors.
num_neighbors : wp.array, shape (total_atoms,), dtype=wp.int32
OUTPUT (atomic): per-atom neighbor counts. Caller must zero
before this call.
wp_dtype : type
Warp scalar dtype (``wp.float32`` or ``wp.float64``).
device : str
Warp device string (e.g. ``"cuda:0"``).
total_cells : int
Sum of ``cells_per_system``. Caller computes once per geometry change.
n_outer : int
Non-self outer cell offsets at ``R_max``. From
:func:`compute_batch_pair_centric_n_outer`.
R_max : tuple[int, int, int]
Cross-system maximum per-axis search radius.
half_fill : bool, default True
If True, half-shell + ``j > i`` self filter; if False, full shell
+ ``j != i`` self filter.
block_dim : int, default 64
Threads per CUDA block.
Notes
-----
- Pair-centric kernels are CUDA-only.
- The same pair-centric kernel factory supports compact ``target_indices``
rows, optional vector/distance buffers, ``pair_fn`` slot outputs, and
selective rebuild flags.
See Also
--------
batch_query_cell_list : Atom-centric alternative
compute_batch_pair_centric_n_outer : Computes ``n_outer``
"""
if "cpu" in str(device).lower():
raise ValueError(
"strategy='pair_centric' is not supported on CPU "
"(kernels use CUDA block scheduling). Pass 'atom_centric' instead.",
)
natom = int(positions.shape[0])
num_systems = int(cell.shape[0])
block_dim_int = int(block_dim)
n_offsets_int = int(n_outer) + 1
logical_blocks, work_per_cuda_block, launch_dim, coarsened = (
_pair_centric_launch_plan(
int(total_cells),
int(n_outer),
block_dim_int,
max_launch_size=max_launch_size,
)
)
R_max_vec = wp.vec3i(int(R_max[0]), int(R_max[1]), int(R_max[2]))
partial = target_indices is not None
rebuild_flags_arg = (
rebuild_flags
if rebuild_flags is not None
else _empty_sentinel(1, wp.bool, device)
)
target_row_lookup_arg = _prepare_target_row_lookup(
target_indices,
target_row_lookup,
natom,
device,
)
(
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
) = _prepare_pair_output_args(
wp_dtype,
device,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
)
wp.launch(
_build_cell_to_system_map,
dim=num_systems,
inputs=[cell_offsets, cells_per_system, cell_to_system],
device=device,
)
wp.launch(
get_cell_list_gather_kernel(wp_dtype),
dim=natom,
inputs=[
positions,
atom_periodic_shifts,
cell_atom_list,
sorted_positions,
sorted_atom_periodic_shifts,
],
device=device,
)
kernel = get_query_cell_list_kernel(
wp_dtype,
strategy="pair_centric",
batched=True,
selective=rebuild_flags is not None,
partial=partial,
half_fill=bool(half_fill),
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
coarsened=coarsened,
)
wp.launch(
kernel,
dim=launch_dim,
block_dim=block_dim_int,
inputs=[
sorted_positions,
sorted_atom_periodic_shifts,
cell,
_empty_sentinel(1, wp.bool, device),
pbc,
wp_dtype(cutoff),
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
_empty_sentinel(1, wp.int32, device),
neighbor_search_radius,
atoms_per_cell_count,
cell_atom_start_indices,
cell_atom_list,
cell_offsets,
cell_to_system,
target_row_lookup_arg,
neighbor_matrix,
neighbor_matrix_shifts,
num_neighbors,
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
block_dim_int,
total_cells,
n_offsets_int,
logical_blocks,
work_per_cuda_block,
R_max_vec,
rebuild_flags_arg,
],
device=device,
)
[docs]
def batch_query_cell_list(
positions: wp.array,
cell: wp.array,
pbc: wp.array,
cutoff: float,
batch_idx: wp.array,
cells_per_dimension: wp.array,
neighbor_search_radius: wp.array,
cell_offsets: wp.array,
atom_periodic_shifts: wp.array,
atom_to_cell_mapping: wp.array,
atoms_per_cell_count: wp.array,
cell_atom_start_indices: wp.array,
cell_atom_list: wp.array,
neighbor_matrix: wp.array,
neighbor_matrix_shifts: wp.array,
num_neighbors: wp.array,
wp_dtype: type,
device: str,
half_fill: bool = False,
rebuild_flags: wp.array | None = None,
*,
sorted_positions: wp.array | None = None,
sorted_atom_periodic_shifts: wp.array | None = None,
strategy: str = "atom_centric",
atom_centric_path: str = "auto",
cells_per_system: wp.array | None = None,
cell_to_system: wp.array | None = None,
n_outer: int | None = None,
R_max: tuple[int, int, int] | None = None,
total_cells: int | None = None,
target_indices: wp.array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: wp.array | None = None,
neighbor_vectors: wp.array | None = None,
neighbor_distances: wp.array | None = None,
pair_energies: wp.array | None = None,
pair_forces: wp.array | None = None,
target_row_lookup: wp.array | None = None,
max_launch_size: int = PAIR_CENTRIC_MAX_LINEAR_LAUNCH,
) -> None:
"""Core warp launcher for querying batch spatial cell lists to build neighbor matrices.
Uses pre-built cell list data structures to efficiently find all atom pairs
within the specified cutoff distance for multiple systems using pure warp
operations. Mirrors the single-system :func:`query_cell_list` signature:
``strategy`` selects which of the two batch query kernels to launch;
pair-centric requires additional caller-allocated scratch + metadata
that the atom-centric path doesn't need.
Parameters
----------
positions : wp.array, shape (total_atoms, 3), dtype=wp.vec3*
Concatenated atomic coordinates for all systems in the batch.
cell : wp.array, shape (num_systems, 3, 3), dtype=wp.mat33*
Unit cell matrices for each system in the batch.
pbc : wp.array, shape (num_systems, 3), dtype=wp.bool
Periodic boundary condition flags for each system and dimension.
cutoff : float
Neighbor search cutoff distance.
batch_idx : wp.array, shape (total_atoms,), dtype=wp.int32
System index for each atom.
cells_per_dimension : wp.array, shape (num_systems, 3), dtype=wp.vec3i
Number of cells in x, y, z directions for each system.
neighbor_search_radius : wp.array, shape (num_systems, 3), dtype=wp.vec3i
Radius of neighboring cells to search for each system.
cell_offsets : wp.array, shape (num_systems,), dtype=wp.int32
Starting index in global cell arrays for each system.
Output from batch_build_cell_list.
atom_periodic_shifts : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
Periodic boundary crossings for each atom. Output from batch_build_cell_list.
atom_to_cell_mapping : wp.array, shape (total_atoms, 3), dtype=wp.vec3i
3D cell coordinates for each atom. Output from batch_build_cell_list.
atoms_per_cell_count : wp.array, shape (max_total_cells,), dtype=wp.int32
Number of atoms in each cell. Output from batch_build_cell_list.
cell_atom_start_indices : wp.array, shape (max_total_cells,), dtype=wp.int32
Starting index in cell_atom_list for each cell. Output from batch_build_cell_list.
cell_atom_list : wp.array, shape (total_atoms,), dtype=wp.int32
Flattened list of atom indices organized by cell. Output from batch_build_cell_list.
neighbor_matrix : wp.array, shape (total_atoms, max_neighbors), dtype=wp.int32
OUTPUT: Neighbor matrix to be filled with neighbor atom indices.
neighbor_matrix_shifts : wp.array, shape (total_atoms, max_neighbors, 3), dtype=wp.vec3i
OUTPUT: Matrix storing shift vectors for each neighbor relationship.
num_neighbors : wp.array, shape (total_atoms,), dtype=wp.int32
OUTPUT (atomic): per-atom neighbor counts. Accumulated via
``wp.atomic_add``, so non-selective callers must zero it before the
call (selective callers zero it via ``rebuild_flags``).
wp_dtype : type
Warp dtype (``wp.float32`` or ``wp.float64``).
device : str
Warp device string (e.g., 'cuda:0', 'cpu').
half_fill : bool, default=False
If True, only store half of the neighbor relationships (i < j).
rebuild_flags : wp.array, shape (num_systems,), dtype=wp.bool, optional
Per-system rebuild flags. If provided, only systems where
``rebuild_flags[i]`` is True are processed; others are skipped on
the GPU without CPU sync. When omitted, the non-selective kernel
specialization is launched and the caller is responsible for
pre-zeroing ``num_neighbors``.
sorted_positions : wp.array, shape (total_atoms,), dtype=wp.vec3*, optional
Per-cell-contiguous gather scratch. Allocated transiently when
omitted; graph/capture callers should pass caller-owned scratch.
sorted_atom_periodic_shifts : wp.array, shape (total_atoms,), dtype=wp.vec3i, optional
Per-cell-contiguous gather scratch. Allocated transiently when
omitted; graph/capture callers should pass caller-owned scratch.
target_indices : wp.array, shape (num_targets,), dtype=wp.int32, optional
Restrict central rows to a subset of atom indices. Output rows are
compact and follow ``target_indices`` order for both strategies.
return_vectors, return_distances : bool, default ``False``
Write per-pair displacement vectors / distances into the
``neighbor_vectors`` / ``neighbor_distances`` kwargs.
pair_fn : callable, optional
Module-scope ``@wp.func`` of signature
``(r_ij, distance, pair_params, i, j) -> (energy, force)``.
pair_params : wp.array, shape (num_atoms, num_parameters), optional
Per-atom pair-function parameters; required with ``pair_fn``.
neighbor_vectors, neighbor_distances : wp.array, optional
OUTPUT buffers for per-pair displacements / distances.
pair_energies, pair_forces : wp.array, optional
OUTPUT buffers for per-pair energies / forces; required with
``pair_fn``.
Notes
-----
- This is a low-level warp interface. For framework bindings, use torch/jax wrappers.
- Output arrays must be pre-allocated by caller.
- Both atom-centric and pair-centric paths consume the per-cell-contiguous
``sorted_positions`` / ``sorted_atom_periodic_shifts`` scratch. The
selected path fills that scratch before launching its neighbor kernel.
- Both strategies support compact ``target_indices`` rows, optional
vector/distance buffers, ``pair_fn`` slot outputs, and selective rebuild
flags.
See Also
--------
batch_build_cell_list : Build cell list data structures (call before this)
batch_query_cell_list_pair_centric_sorted : Pair-centric alternative (CUDA only).
"""
total_atoms = positions.shape[0]
selective = rebuild_flags is not None
rebuild_flags_arg = (
rebuild_flags
if rebuild_flags is not None
else _empty_sentinel(1, wp.bool, device)
)
if strategy == "pair_centric":
missing = [
name
for name, val in (
("cells_per_system", cells_per_system),
("cell_to_system", cell_to_system),
("n_outer", n_outer),
("R_max", R_max),
("total_cells", total_cells),
)
if val is None
]
if missing:
raise ValueError(
f"strategy='pair_centric' requires the following kwargs to "
f"be provided (caller-allocated scratch + sync-computed "
f"metadata): {missing}. See compute_batch_pair_centric_n_outer "
f"for n_outer.",
)
elif strategy != "atom_centric":
raise ValueError(
f"strategy must be 'atom_centric' | 'pair_centric', got {strategy!r}",
)
if atom_centric_path == "auto":
atom_centric_path = "direct"
elif atom_centric_path not in {"direct", "sorted"}:
raise ValueError(
"atom_centric_path must be 'auto' | 'direct' | 'sorted', "
f"got {atom_centric_path!r}",
)
needs_sorted = strategy == "pair_centric" or atom_centric_path == "sorted"
_vec_dtype, _ = dtype_info(wp_dtype)
if needs_sorted:
if (sorted_positions is None) != (sorted_atom_periodic_shifts is None):
raise ValueError(
"Pass both sorted_positions and sorted_atom_periodic_shifts, "
"or neither - got a mixed state.",
)
if sorted_positions is None:
sorted_positions = wp.empty(
int(total_atoms), dtype=_vec_dtype, device=device
)
if sorted_atom_periodic_shifts is None:
sorted_atom_periodic_shifts = wp.empty(
int(total_atoms), dtype=wp.vec3i, device=device
)
else:
sorted_positions = _empty_sentinel(1, _vec_dtype, device)
sorted_atom_periodic_shifts = _empty_sentinel(1, wp.vec3i, device)
if strategy == "pair_centric":
batch_query_cell_list_pair_centric_sorted(
positions=positions,
cell=cell,
pbc=pbc,
cutoff=cutoff,
cells_per_dimension=cells_per_dimension,
neighbor_search_radius=neighbor_search_radius,
cell_offsets=cell_offsets,
cells_per_system=cells_per_system,
atom_periodic_shifts=atom_periodic_shifts,
atoms_per_cell_count=atoms_per_cell_count,
cell_atom_start_indices=cell_atom_start_indices,
cell_atom_list=cell_atom_list,
sorted_positions=sorted_positions,
sorted_atom_periodic_shifts=sorted_atom_periodic_shifts,
cell_to_system=cell_to_system,
neighbor_matrix=neighbor_matrix,
neighbor_matrix_shifts=neighbor_matrix_shifts,
num_neighbors=num_neighbors,
wp_dtype=wp_dtype,
device=device,
total_cells=int(total_cells),
n_outer=int(n_outer),
R_max=R_max,
half_fill=bool(half_fill),
target_indices=target_indices,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
rebuild_flags=rebuild_flags,
target_row_lookup=target_row_lookup,
max_launch_size=max_launch_size,
)
return
if needs_sorted:
wp.launch(
get_cell_list_gather_kernel(wp_dtype),
dim=total_atoms,
inputs=[
positions,
atom_periodic_shifts,
cell_atom_list,
sorted_positions,
sorted_atom_periodic_shifts,
],
device=device,
)
partial = target_indices is not None
if selective and not partial:
selective_zero_num_neighbors(num_neighbors, batch_idx, rebuild_flags, device)
target_indices_arg = (
target_indices
if target_indices is not None
else _empty_sentinel(1, wp.int32, device)
)
(
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
) = _prepare_pair_output_args(
wp_dtype,
device,
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
pair_params=pair_params,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_energies=pair_energies,
pair_forces=pair_forces,
)
kernel = get_query_cell_list_kernel(
wp_dtype,
strategy="atom_centric",
batched=True,
selective=selective,
partial=partial,
half_fill=bool(half_fill),
return_vectors=return_vectors,
return_distances=return_distances,
pair_fn=pair_fn,
atom_centric_path=atom_centric_path,
)
dim = int(target_indices.shape[0]) if partial else total_atoms
wp.launch(
kernel,
dim=dim,
inputs=[
positions,
atom_periodic_shifts,
sorted_positions,
sorted_atom_periodic_shifts,
cell,
_empty_sentinel(1, wp.bool, device),
pbc,
batch_idx,
wp_dtype(cutoff),
_empty_sentinel(1, wp.int32, device),
cells_per_dimension,
_empty_sentinel(1, wp.int32, device),
neighbor_search_radius,
atom_to_cell_mapping,
atoms_per_cell_count,
cell_atom_start_indices,
cell_atom_list,
cell_offsets,
target_indices_arg,
neighbor_matrix,
neighbor_matrix_shifts,
num_neighbors,
neighbor_vectors_arg,
neighbor_distances_arg,
pair_params_arg,
pair_energies_arg,
pair_forces_arg,
rebuild_flags_arg,
],
device=device,
)