Source code for nvalchemiops.torch.neighbors.batch_cluster_tile

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

"""PyTorch bindings for batched cluster-pair tile neighbor list.

All torch-side work (per-system Morton sort with system-major key,
per-system padding to a multiple of ``TILE_GROUP_SIZE``, SoA gather of
sorted positions, ``inv_cell_batch`` inversion, ``wp.from_torch``
conversion) lives in this module; the Warp layer at
``nvalchemiops.neighbors.cluster_tile`` only sees ``wp.array``
inputs.

Scope: float32, orthorhombic or triclinic PBC, one cell per system,
arbitrary natom per system (padded internally).
"""

from typing import TYPE_CHECKING

import torch
import warp as wp

from nvalchemiops.neighbors.cluster_tile import (
    TILE_GROUP_SIZE,
)
from nvalchemiops.neighbors.cluster_tile import (
    batch_build_cluster_tile_list as wp_batch_build_cluster_tile_list,
)
from nvalchemiops.neighbors.cluster_tile import (
    batch_query_cluster_tile as wp_batch_query_cluster_tile,
)
from nvalchemiops.neighbors.cluster_tile import (
    batch_query_cluster_tile_coo as wp_batch_query_cluster_tile_coo,
)
from nvalchemiops.neighbors.cluster_tile import (
    estimate_batch_cluster_tile_segments as wp_estimate_batch_cluster_tile_segments,
)
from nvalchemiops.neighbors.cluster_tile import (
    estimate_batch_max_tiles_per_group as wp_estimate_batch_max_tiles_per_group,
)
from nvalchemiops.neighbors.neighbor_utils import (
    NeighborOverflowError,
    estimate_max_neighbors,
)
from nvalchemiops.neighbors.neighbor_utils import (
    fill_neighbor_matrix_tail as wp_fill_neighbor_matrix_tail,
)
from nvalchemiops.neighbors.neighbor_utils import (
    selective_zero_num_neighbors as wp_selective_zero_num_neighbors,
)
from nvalchemiops.neighbors.output_args import _has_partial_or_pair_outputs
from nvalchemiops.torch.types import get_wp_dtype

if TYPE_CHECKING:
    from nvalchemiops.torch.neighbors._autograd import _NeighborForwardOutput

__all__ = [
    "TILE_GROUP_SIZE",
    "estimate_batch_max_tiles_per_group",
    "estimate_batch_cluster_tile_list_sizes",
    "estimate_batch_cluster_tile_segments",
    "allocate_batch_cluster_tile_list",
    "batch_build_cluster_tile_list",
    "batch_query_cluster_tile",
    "batch_query_cluster_tile_coo",
    "batch_cluster_tile_neighbor_list",
]


@torch.library.custom_op(
    "nvalchemiops::_batch_cluster_tile_fill_neighbor_matrix_tail",
    mutates_args=("neighbor_matrix",),
)
def _batch_cluster_tile_fill_neighbor_matrix_tail_op(
    num_neighbors: torch.Tensor,
    neighbor_matrix: torch.Tensor,
    n_rows: int,
    max_neighbors: int,
    fill_value: int,
) -> None:
    if max_neighbors <= 0:
        return
    wp_fill_neighbor_matrix_tail(
        wp.from_torch(
            num_neighbors, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        int(n_rows),
        int(max_neighbors),
        int(fill_value),
        wp.from_torch(
            neighbor_matrix, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        str(neighbor_matrix.device),
    )


@_batch_cluster_tile_fill_neighbor_matrix_tail_op.register_fake
def _(
    num_neighbors: torch.Tensor,
    neighbor_matrix: torch.Tensor,
    n_rows: int,
    max_neighbors: int,
    fill_value: int,
) -> None:
    return None


# =============================================================================
# Sizing + allocation helpers
# =============================================================================
[docs] def estimate_batch_max_tiles_per_group( batch_ptr: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, *, safety: float = 2.0, floor: int = 256, ) -> int: """Estimate batched ``max_tiles_per_group`` from per-system cells. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,) Cumulative atom counts. cutoff : float Cartesian cutoff used for cluster-tile construction. cell_batch : torch.Tensor, shape (num_systems, 3, 3) Per-system cell matrices. safety : float, default 2.0 Multiplier on the volumetric estimate. floor : int, default 256 Minimum returned value for batched compact buffers. Returns ------- int Shared ``max_tiles_per_group`` for the batched compact tile buffer. """ if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") volumes = torch.linalg.det(cell_batch.to(torch.float64)).abs().view(-1) return wp_estimate_batch_max_tiles_per_group( batch_ptr, cutoff, volumes, safety=safety, floor=floor, )
[docs] def estimate_batch_cluster_tile_list_sizes( batch_ptr: torch.Tensor, max_tiles_per_group: int = 256, ) -> tuple[int, int, int, int, int]: """Estimate allocation sizes for the batched tile neighbor list state. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative atom counts defining per-system ranges. max_tiles_per_group : int, default 256 Upper bound on neighbor groups per row_group (dense-cutoff cap). Returns ------- n_padded : int Total padded atom count (sum of per-system ``ceil(natom/32)*32``). ngroup : int Number of 32-atom groups: ``n_padded // 32``. ngroup_padded : int Group-array pad length for in-bounds ``wp.tile_load`` at any TILE-aligned offset. max_tiles : int Upper bound on the tile pair list size. num_systems : int """ if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") num_systems = int(batch_ptr.shape[0]) - 1 natom_per_system = (batch_ptr[1:] - batch_ptr[:-1]).to(torch.int64) natom_padded_per_system = ( (natom_per_system + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE n_padded = int(natom_padded_per_system.sum().item()) ngroup = n_padded // TILE_GROUP_SIZE ngroup_padded = ( (ngroup + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE + TILE_GROUP_SIZE max_tiles = ngroup * min(ngroup, max_tiles_per_group) return n_padded, ngroup, ngroup_padded, max_tiles, num_systems
[docs] def estimate_batch_cluster_tile_segments( batch_ptr: torch.Tensor, max_neighbors: int, max_tiles_per_group: int = 256, ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]: """Estimate fixed per-system tile and COO segment buffers. Returns ``(tile_capacities, tile_offsets, pair_capacities, pair_offsets)`` as int32 tensors on ``batch_ptr.device``. ``tile_offsets`` and ``pair_offsets`` are caller-owned fixed inputs for segmented cluster-tile build / COO query paths; ``tile_counts`` and ``pair_counts`` are separate output counters with length ``num_systems``. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative atom counts defining per-system ranges. max_neighbors : int Upper bound on neighbors per atom used to size per-system COO segments. max_tiles_per_group : int, default 256 Upper bound on neighbor groups per row_group (dense-cutoff cap). Returns ------- tile_capacities : torch.Tensor, shape (num_systems,), dtype=int32 Per-system tile buffer capacities. tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR-style offsets into the segmented tile buffer (fixed input to build). pair_capacities : torch.Tensor, shape (num_systems,), dtype=int32 Per-system pair buffer capacities. pair_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR-style offsets into the segmented COO pair buffer (fixed input to query). """ if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") tile_caps, tile_offsets, pair_caps, pair_offsets = ( wp_estimate_batch_cluster_tile_segments( batch_ptr, max_neighbors=int(max_neighbors), max_tiles_per_group=int(max_tiles_per_group), ) ) device = batch_ptr.device return ( torch.tensor(tile_caps, dtype=torch.int32, device=device), torch.tensor(tile_offsets, dtype=torch.int32, device=device), torch.tensor(pair_caps, dtype=torch.int32, device=device), torch.tensor(pair_offsets, dtype=torch.int32, device=device), )
[docs] def allocate_batch_cluster_tile_list( batch_ptr: torch.Tensor, device: torch.device, dtype: torch.dtype = torch.float32, max_tiles_per_group: int = 256, ) -> tuple[ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, ]: """Allocate all state tensors consumed by ``batch_build_cluster_tile_list``. Parameters ---------- batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative atom counts defining per-system ranges. device : torch.device Target device for all allocated tensors. dtype : torch.dtype, default torch.float32 Floating-point dtype for position and group-centroid tensors. max_tiles_per_group : int, default 256 Upper bound on neighbor groups per row_group; controls the tile buffer size via :func:`estimate_batch_cluster_tile_list_sizes`. Returns ------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation mapping padded slot to original atom index; sentinel value ``N`` (total real atoms) marks padding slots. sort_inv : torch.Tensor, shape (N,), dtype=int32 Inverse permutation over the real (un-padded) atoms. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=dtype Morton-sorted x-coordinates in the padded SoA layout. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=dtype Morton-sorted y-coordinates in the padded SoA layout. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=dtype Morton-sorted z-coordinates in the padded SoA layout. batch_idx_sorted : torch.Tensor, shape (n_padded,), dtype=int32 System index for every padded slot after Morton sort. batch_ptr_padded : torch.Tensor, shape (num_systems + 1,), dtype=int32 Cumulative padded atom counts (multiples of ``TILE_GROUP_SIZE``). group_system : torch.Tensor, shape (ngroup,), dtype=int32 System index for each 32-atom group. group_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR group-level pointer derived from ``batch_ptr_padded``. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box centre x-coordinate (zeroed; written by build). group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box centre y-coordinate (zeroed; written by build). group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box centre z-coordinate (zeroed; written by build). group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box half-extent x (zeroed; written by build). group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box half-extent y (zeroed; written by build). group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=dtype Group bounding-box half-extent z (zeroed; written by build). num_tiles : torch.Tensor, shape (1,), dtype=int32 Scalar tile-pair count; written by build, read by query. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index for each tile pair (zeroed; written by build). tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index for each tile pair (zeroed; written by build). tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 System index for each tile pair (zeroed; written by build). """ n_padded, ngroup, ngroup_padded, max_tiles, num_systems = ( estimate_batch_cluster_tile_list_sizes( batch_ptr, max_tiles_per_group=max_tiles_per_group, ) ) N = int(batch_ptr[-1].item()) sorted_atom_index = torch.empty(n_padded, dtype=torch.int32, device=device) sort_inv = torch.empty(N, dtype=torch.int32, device=device) sorted_pos_x = torch.empty(n_padded, dtype=dtype, device=device) sorted_pos_y = torch.empty(n_padded, dtype=dtype, device=device) sorted_pos_z = torch.empty(n_padded, dtype=dtype, device=device) batch_idx_sorted = torch.empty(n_padded, dtype=torch.int32, device=device) batch_ptr_padded = torch.empty(num_systems + 1, dtype=torch.int32, device=device) group_system = torch.empty(ngroup, dtype=torch.int32, device=device) group_ptr = torch.empty(num_systems + 1, dtype=torch.int32, device=device) group_ctr_x = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ctr_y = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ctr_z = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ext_x = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ext_y = torch.zeros(ngroup_padded, dtype=dtype, device=device) group_ext_z = torch.zeros(ngroup_padded, dtype=dtype, device=device) num_tiles = torch.zeros(1, dtype=torch.int32, device=device) tile_row_group = torch.zeros(max_tiles, dtype=torch.int32, device=device) tile_col_group = torch.zeros(max_tiles, dtype=torch.int32, device=device) tile_system = torch.zeros(max_tiles, dtype=torch.int32, device=device) return ( sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, )
# ============================================================================= # Morton sort + padded scatter (torch side) # ============================================================================= def _per_atom_morton_codes( positions: torch.Tensor, batch_idx: torch.Tensor, inv_cell_batch: torch.Tensor, ) -> torch.Tensor: """30-bit Morton codes using per-atom fractional coords (triclinic-safe).""" inv_per_atom = inv_cell_batch[batch_idx] frac = torch.einsum("ni,nij->nj", positions, inv_per_atom) frac = frac - torch.floor(frac) bucket = (frac * 1024.0).clamp(0, 1023).to(torch.int32) ix, iy, iz = bucket.unbind(dim=-1) def _spread(x: torch.Tensor) -> torch.Tensor: x = x & 0x3FF x = (x | (x << 16)) & 0x030000FF x = (x | (x << 8)) & 0x0300F00F x = (x | (x << 4)) & 0x030C30C3 x = (x | (x << 2)) & 0x09249249 return x return (_spread(iz) << 2) | (_spread(iy) << 1) | _spread(ix) def _batched_morton_sort_padded( positions: torch.Tensor, batch_idx: torch.Tensor, batch_ptr: torch.Tensor, inv_cell_batch: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, ) -> None: """Per-system Morton sort into the padded layout (torch ops only).""" N = positions.shape[0] num_systems = inv_cell_batch.shape[0] device = positions.device natom_per_system = batch_ptr[1:] - batch_ptr[:-1] natom_padded_per_system = ( (natom_per_system + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE bpp = torch.zeros(num_systems + 1, dtype=torch.int32, device=device) torch.cumsum(natom_padded_per_system, dim=0, out=bpp[1:]) batch_ptr_padded.copy_(bpp) codes = _per_atom_morton_codes(positions, batch_idx, inv_cell_batch) # System-major key: system index in high 32 bits, Morton code in low. system_major = codes.to(torch.int64) | (batch_idx.to(torch.int64) << 32) sorted_atom_index_real = torch.argsort(system_major).to(torch.int32) # Inverse permutation over real atoms. inv = torch.empty(N, dtype=torch.int32, device=device) inv[sorted_atom_index_real] = torch.arange( N, dtype=torch.int32, device=device, ) sort_inv.copy_(inv) # Placement indices for each real atom in the padded layout. batch_idx_sorted_real = batch_idx[sorted_atom_index_real] within_system = ( torch.arange( N, dtype=torch.int32, device=device, ) - batch_ptr[batch_idx_sorted_real] ) padded_slot = bpp[batch_idx_sorted_real] + within_system # sorted_atom_index[k] = N is the padding sentinel. sp = torch.full( (sorted_atom_index.shape[0],), N, dtype=torch.int32, device=device, ) sp[padded_slot] = sorted_atom_index_real sorted_atom_index.copy_(sp) # System index for every padded slot. bis = ( torch.repeat_interleave( torch.arange(num_systems, dtype=torch.int32, device=device), natom_padded_per_system, ) .to(torch.int32) .contiguous() ) batch_idx_sorted.copy_(bis) # Padding slots duplicate each system's first real atom, so # tile_min / tile_max over the group remain well-formed. Duplicates # cannot cause a miss because they add a point already inside the # system's spatial region. first_atom_per_system = batch_ptr[:-1] position_index = torch.where( sp == N, first_atom_per_system[bis], sp, ) sorted_pos = positions[position_index].contiguous() sorted_pos_x.copy_(sorted_pos[:, 0].contiguous()) sorted_pos_y.copy_(sorted_pos[:, 1].contiguous()) sorted_pos_z.copy_(sorted_pos[:, 2].contiguous()) # ============================================================================= # Component ops (torch wrappers around the warp launchers) # ============================================================================= @torch.library.custom_op( "nvalchemiops::_batch_build_cluster_tile_list", mutates_args=( "sorted_atom_index", "sort_inv", "sorted_pos_x", "sorted_pos_y", "sorted_pos_z", "batch_idx_sorted", "batch_ptr_padded", "group_system", "group_ptr", "group_ctr_x", "group_ctr_y", "group_ctr_z", "group_ext_x", "group_ext_y", "group_ext_z", "num_tiles", "tile_row_group", "tile_col_group", "tile_system", "tile_counts", ), ) def _batch_build_cluster_tile_list_op( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, batch_idx: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, ) -> None: wp_device = str(positions.device) wp_dtype = get_wp_dtype(positions.dtype) _batched_morton_sort_padded( positions, batch_idx, batch_ptr, inv_cell_batch, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, ) group_ptr.copy_((batch_ptr_padded // TILE_GROUP_SIZE).to(torch.int32).contiguous()) group_system.copy_( batch_idx_sorted[::TILE_GROUP_SIZE].to(torch.int32).contiguous(), ) if not use_segmented: num_tiles.zero_() group_ctr_x.zero_() group_ctr_y.zero_() group_ctr_z.zero_() group_ext_x.zero_() group_ext_y.zero_() group_ext_z.zero_() wp_batch_build_cluster_tile_list( sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, return_ctype=True), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, return_ctype=True), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, return_ctype=True), group_system=wp.from_torch(group_system, dtype=wp.int32, return_ctype=True), group_ptr=wp.from_torch(group_ptr, dtype=wp.int32, return_ctype=True), cell_batch=wp.from_torch( cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), inv_cell_batch=wp.from_torch( inv_cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), cutoff=float(cutoff), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ), tile_system=wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True), wp_dtype=wp_dtype, device=wp_device, rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True) if use_rebuild_flags else None ), tile_offsets=( wp.from_torch(tile_offsets, dtype=wp.int32, return_ctype=True) if use_segmented else None ), tile_counts=( wp.from_torch(tile_counts, dtype=wp.int32, return_ctype=True) if use_segmented else None ), group_ctr_x_buffer=wp.from_torch( group_ctr_x, dtype=wp_dtype, return_ctype=True ), group_ctr_y_buffer=wp.from_torch( group_ctr_y, dtype=wp_dtype, return_ctype=True ), group_ctr_z_buffer=wp.from_torch( group_ctr_z, dtype=wp_dtype, return_ctype=True ), group_ext_x_buffer=wp.from_torch( group_ext_x, dtype=wp_dtype, return_ctype=True ), group_ext_y_buffer=wp.from_torch( group_ext_y, dtype=wp_dtype, return_ctype=True ), group_ext_z_buffer=wp.from_torch( group_ext_z, dtype=wp_dtype, return_ctype=True ), ) @_batch_build_cluster_tile_list_op.register_fake def _( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, batch_ptr: torch.Tensor, batch_idx: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, rebuild_flags: torch.Tensor, tile_offsets: torch.Tensor, tile_counts: torch.Tensor, use_rebuild_flags: bool, use_segmented: bool, ) -> None: return None
[docs] def batch_build_cluster_tile_list( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, batch_ptr: torch.Tensor, sorted_atom_index: torch.Tensor, sort_inv: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, batch_idx_sorted: torch.Tensor, batch_ptr_padded: torch.Tensor, group_system: torch.Tensor, group_ptr: torch.Tensor, group_ctr_x: torch.Tensor, group_ctr_y: torch.Tensor, group_ctr_z: torch.Tensor, group_ext_x: torch.Tensor, group_ext_y: torch.Tensor, group_ext_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, ) -> None: """Build batched tile neighbor list state into pre-allocated outputs. Runs the per-system Morton sort + padded SoA gather in torch, then the bbox reduction + tile-pair enumeration in warp. All output tensors are modified in-place. Parameters ---------- positions : torch.Tensor, shape (N, 3), dtype=float32 Concatenated atomic coordinates across all systems. cutoff : float Cartesian cutoff radius used for tile-pair enumeration. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 CSR pointer giving atom ranges per system. sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Output permutation array; modified in-place. sort_inv : torch.Tensor, shape (N,), dtype=int32 Output inverse permutation; modified in-place. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Output SoA x-positions; modified in-place. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Output SoA y-positions; modified in-place. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Output SoA z-positions; modified in-place. batch_idx_sorted : torch.Tensor, shape (n_padded,), dtype=int32 Output system index per padded slot; modified in-place. batch_ptr_padded : torch.Tensor, shape (num_systems + 1,), dtype=int32 Output padded CSR pointer; modified in-place. group_system : torch.Tensor, shape (ngroup,), dtype=int32 Output system index per group; modified in-place. group_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 Output group-level CSR pointer; modified in-place. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box centre x; modified in-place. group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box centre y; modified in-place. group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box centre z; modified in-place. group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box half-extent x; modified in-place. group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box half-extent y; modified in-place. group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output group bounding-box half-extent z; modified in-place. num_tiles : torch.Tensor, shape (1,), dtype=int32 Output tile-pair count (scalar); modified in-place. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Output row group per tile pair; modified in-place. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Output column group per tile pair; modified in-place. tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 Output system index per tile pair; modified in-place. inv_cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32, optional Pre-computed inverse of ``cell_batch``. Computed internally when None. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=bool, optional Per-system selective rebuild flags. Requires ``tile_offsets`` and ``tile_counts`` (segmented path). tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system tile segment offsets for the segmented build path. Must be provided together with ``tile_counts``. tile_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Output per-system tile counts for the segmented build path; modified in-place. Must be provided together with ``tile_offsets``. """ if positions.dtype != torch.float32: raise TypeError("positions must be float32") if ( cell_batch.dtype != torch.float32 or cell_batch.ndim != 3 or cell_batch.shape[1:] != (3, 3) ): raise ValueError( f"cell_batch must be (S, 3, 3) float32; got {tuple(cell_batch.shape)}", ) if batch_ptr.dtype != torch.int32 or batch_ptr.ndim != 1: raise ValueError("batch_ptr must be 1D int32") if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") num_systems = cell_batch.shape[0] if batch_ptr.shape[0] != num_systems + 1: raise ValueError( f"batch_ptr length {batch_ptr.shape[0]} != num_systems+1 = {num_systems + 1}", ) N = positions.shape[0] if int(batch_ptr[-1].item()) != N: raise ValueError(f"batch_ptr[-1] ({int(batch_ptr[-1])}) != N ({N})") if inv_cell_batch is None: inv_cell_batch = torch.linalg.inv(cell_batch).contiguous() batch_idx = torch.repeat_interleave( torch.arange(num_systems, dtype=torch.int32, device=positions.device), (batch_ptr[1:] - batch_ptr[:-1]).to(torch.int64), ) use_segmented = (tile_offsets is not None) or (tile_counts is not None) if (tile_offsets is None) != (tile_counts is None): raise ValueError("Pass both 'tile_offsets' and 'tile_counts', or neither.") use_rebuild_flags = rebuild_flags is not None if use_rebuild_flags and not use_segmented: raise ValueError( "rebuild_flags requires tile_offsets and tile_counts for batch_cluster_tile" ) dummy_i32 = torch.empty(1, dtype=torch.int32, device=positions.device) dummy_bool = torch.empty(1, dtype=torch.bool, device=positions.device) _batch_build_cluster_tile_list_op( positions, float(cutoff), cell_batch, inv_cell_batch, batch_ptr, batch_idx, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, rebuild_flags if rebuild_flags is not None else dummy_bool, tile_offsets if tile_offsets is not None else dummy_i32, tile_counts if tile_counts is not None else dummy_i32, bool(use_rebuild_flags), bool(use_segmented), )
@torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile", mutates_args=("neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts"), ) def _batch_query_cluster_tile_op( cutoff: float, natom: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, n_tiles: int, ) -> None: # ``n_tiles`` (host-synced compact tile count) tightens the launch; the # kernel still guards per-tile defensively. ``n_tiles <= 0`` (segmented # path, count unknown here) falls back to the full-buffer launch. wp_device = str(sorted_pos_x.device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_batch_query_cluster_tile( n_tiles=int(n_tiles) if n_tiles > 0 else None, sorted_atom_index=wp.from_torch( sorted_atom_index, dtype=wp.int32, return_ctype=True ), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, return_ctype=True), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, return_ctype=True), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, return_ctype=True), cell_batch=wp.from_torch( cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), inv_cell_batch=wp.from_torch( inv_cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ), tile_system=wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True), cutoff=float(cutoff), natom=int(natom), neighbor_matrix=wp.from_torch( neighbor_matrix, dtype=wp.int32, return_ctype=True, ), num_neighbors=wp.from_torch( num_neighbors, dtype=wp.int32, return_ctype=True, ), neighbor_matrix_shifts=wp.from_torch( neighbor_matrix_shifts, dtype=wp.int32, return_ctype=True, ), wp_dtype=wp_dtype, device=wp_device, ) @_batch_query_cluster_tile_op.register_fake def _( cutoff: float, natom: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, n_tiles: int, ) -> None: return None
[docs] def batch_query_cluster_tile( sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, cutoff: float, natom: int, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, *, cutoff2: float | None = None, neighbor_matrix2: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, batch_idx: torch.Tensor | None = None, return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, ) -> None: """Convert the batched tile pair list to neighbor_matrix in place. Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. See :func:`nvalchemiops.neighbors.cluster_tile.batch_query_cluster_tile` for the full pair-output kwarg semantics. Pair outputs follow the same pattern as the single-system :func:`query_cluster_tile` binding: when any pair-output kwarg is set the call bypasses the torch custom op and forwards directly to the warp launcher (custom ops cannot carry callable ``pair_fn`` across their schema boundary). Parameters ---------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation from padded slot to original atom index (sentinel ``N`` marks padding slots). sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted x-coordinates in the padded SoA layout. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted y-coordinates in the padded SoA layout. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted z-coordinates in the padded SoA layout. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. num_tiles : torch.Tensor, shape (1,), dtype=int32 Scalar tile-pair count produced by ``batch_build_cluster_tile_list``. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index per tile pair. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index per tile pair. tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 System index per tile pair. cutoff : float Cartesian cutoff radius for pair filtering. natom : int Total number of real atoms across all systems. neighbor_matrix : torch.Tensor, shape (natom, max_neighbors), dtype=int32 Output neighbor indices; modified in-place. num_neighbors : torch.Tensor, shape (natom,), dtype=int32 Output per-atom neighbor counts; modified in-place. neighbor_matrix_shifts : torch.Tensor, shape (natom, max_neighbors, 3), dtype=int32 Output periodic image shift vectors; modified in-place. inv_cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32, optional Pre-computed inverse of ``cell_batch``. Computed internally when None. cutoff2 : float, optional Secondary cutoff for a second neighbor matrix. Matrix-format only. neighbor_matrix2 : torch.Tensor, shape (natom, max_neighbors), dtype=int32, optional Output neighbor indices for the secondary cutoff; modified in-place. num_neighbors2 : torch.Tensor, shape (natom,), dtype=int32, optional Output per-atom counts for the secondary cutoff; modified in-place. neighbor_matrix_shifts2 : torch.Tensor, shape (natom, max_neighbors, 3), dtype=int32, optional Output shift vectors for the secondary cutoff; modified in-place. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=bool, optional Per-system selective rebuild flags. Requires ``batch_idx``, ``tile_offsets``, and ``tile_counts``. tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system tile segment offsets for the segmented path. tile_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Per-system tile counts for the segmented path. batch_idx : torch.Tensor, shape (natom,), dtype=int32, optional System index per atom; required when ``rebuild_flags`` is provided. return_vectors : bool, default False Write per-pair displacement vectors to ``neighbor_vectors``. return_distances : bool, default False Write per-pair scalar distances to ``neighbor_distances``. pair_fn : wp.Function, optional Module-scope Warp function evaluated for each pair. pair_params : torch.Tensor, optional Per-atom parameters forwarded to ``pair_fn``. neighbor_vectors : torch.Tensor, shape (natom, max_neighbors, 3), optional Output displacement vectors; modified in-place when ``return_vectors``. neighbor_distances : torch.Tensor, shape (natom, max_neighbors), optional Output scalar distances; modified in-place when ``return_distances``. pair_energies : torch.Tensor, shape (natom, max_neighbors), optional Output pair energies; modified in-place when ``pair_fn`` is set. pair_forces : torch.Tensor, shape (natom, max_neighbors, 3), optional Output pair forces; modified in-place when ``pair_fn`` is set. """ if inv_cell_batch is None: inv_cell_batch = torch.linalg.inv(cell_batch).contiguous() # Host-sync the tile count to tighten the launch and raise on tile-buffer # overflow. The default (non-selective, non-COO-segmented) batch path uses # a single compact ``num_tiles`` counter with contiguous tile writes, so the # tight launch and a single global overflow check apply. The segmented path # (selective rebuild / segmented COO) keeps the full-buffer launch and is # overflow-checked per system below. tile_capacity = int(tile_row_group.shape[0]) if tile_offsets is None: if torch.compiler.is_compiling(): n_tiles = tile_capacity else: n_tiles = int(num_tiles.item()) if n_tiles > tile_capacity: raise NeighborOverflowError(tile_capacity, n_tiles) else: n_tiles = 0 # segmented: count is per-system; full-buffer launch if tile_counts is not None: seg_caps = tile_offsets[1:] - tile_offsets[:-1] overflow = tile_counts > seg_caps if bool(overflow.any().item()): isys = int(torch.nonzero(overflow, as_tuple=False)[0, 0].item()) raise NeighborOverflowError( int(seg_caps[isys].item()), int(tile_counts[isys].item()), system_index=isys, ) needs_direct = ( cutoff2 is not None or rebuild_flags is not None or tile_offsets is not None or tile_counts is not None or _has_partial_or_pair_outputs( 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, ) ) if needs_direct: if rebuild_flags is not None: if batch_idx is None: raise ValueError("batch_idx is required when rebuild_flags is provided") wp_selective_zero_num_neighbors( wp.from_torch(num_neighbors, dtype=wp.int32, return_ctype=True), wp.from_torch(batch_idx, dtype=wp.int32, return_ctype=True), wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), str(sorted_pos_x.device), ) if cutoff2 is not None and num_neighbors2 is not None: wp_selective_zero_num_neighbors( wp.from_torch(num_neighbors2, dtype=wp.int32, return_ctype=True), wp.from_torch(batch_idx, dtype=wp.int32, return_ctype=True), wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), str(sorted_pos_x.device), ) _batch_query_cluster_tile_optional( cell_batch, inv_cell_batch, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, n_tiles=n_tiles, cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, 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, ) return _batch_query_cluster_tile_op( cutoff, natom, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, inv_cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, n_tiles, )
def _batch_query_cluster_tile_optional( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, *, n_tiles: int, cutoff2: float | None, neighbor_matrix2: torch.Tensor | None, num_neighbors2: torch.Tensor | None, neighbor_matrix_shifts2: torch.Tensor | None, rebuild_flags: torch.Tensor | None, tile_offsets: torch.Tensor | None, tile_counts: torch.Tensor | None, return_vectors: bool, return_distances: bool, pair_fn: wp.Function | None, pair_params: torch.Tensor | None, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, pair_energies: torch.Tensor | None, pair_forces: torch.Tensor | None, ) -> None: """Pair-output path for the batched binding. Bypasses the torch custom op and calls the warp launcher directly so a callable ``pair_fn`` can cross the binding boundary. ``n_tiles`` (compact tile count, or ``<= 0`` for the segmented full-buffer launch) sets the launch dimension; the kernel still guards per-tile defensively. """ device = sorted_pos_x.device wp_device = str(device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_vec_dtype = wp.vec3f wp_pair_params = ( wp.from_torch(pair_params, dtype=wp_dtype) if pair_params is not None else None ) wp_neighbor_vectors = ( wp.from_torch(neighbor_vectors, dtype=wp_vec_dtype) if neighbor_vectors is not None else None ) wp_neighbor_distances = ( wp.from_torch(neighbor_distances, dtype=wp_dtype) if neighbor_distances is not None else None ) wp_pair_energies = ( wp.from_torch(pair_energies, dtype=wp_dtype) if pair_energies is not None else None ) wp_pair_forces = ( wp.from_torch(pair_forces, dtype=wp_vec_dtype) if pair_forces is not None else None ) wp_batch_query_cluster_tile( sorted_atom_index=wp.from_torch(sorted_atom_index, dtype=wp.int32), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype), cell_batch=wp.from_torch(cell_batch.contiguous(), dtype=wp.mat33f), inv_cell_batch=wp.from_torch(inv_cell_batch.contiguous(), dtype=wp.mat33f), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32), tile_col_group=wp.from_torch(tile_col_group, dtype=wp.int32), tile_system=wp.from_torch(tile_system, dtype=wp.int32), cutoff=float(cutoff), natom=int(natom), neighbor_matrix=wp.from_torch(neighbor_matrix, dtype=wp.int32), num_neighbors=wp.from_torch(num_neighbors, dtype=wp.int32), neighbor_matrix_shifts=wp.from_torch(neighbor_matrix_shifts, dtype=wp.int32), wp_dtype=wp_dtype, device=wp_device, n_tiles=int(n_tiles) if n_tiles > 0 else None, cutoff2=cutoff2, neighbor_matrix2=( wp.from_torch(neighbor_matrix2, dtype=wp.int32) if neighbor_matrix2 is not None else None ), num_neighbors2=( wp.from_torch(num_neighbors2, dtype=wp.int32) if num_neighbors2 is not None else None ), neighbor_matrix_shifts2=( wp.from_torch(neighbor_matrix_shifts2, dtype=wp.int32) if neighbor_matrix_shifts2 is not None else None ), rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool) if rebuild_flags is not None else None ), tile_offsets=( wp.from_torch(tile_offsets, dtype=wp.int32) if tile_offsets is not None else None ), tile_counts=( wp.from_torch(tile_counts, dtype=wp.int32) if tile_counts is not None else None ), return_vectors=bool(return_vectors), return_distances=bool(return_distances), pair_fn=pair_fn, pair_params=wp_pair_params, neighbor_vectors=wp_neighbor_vectors, neighbor_distances=wp_neighbor_distances, pair_energies=wp_pair_energies, pair_forces=wp_pair_forces, ) @torch.library.custom_op( "nvalchemiops::_batch_query_cluster_tile_coo", mutates_args=("pair_counter", "coo_list", "coo_shifts"), ) def _batch_query_cluster_tile_coo_op( cutoff: float, natom: int, max_pairs: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, ) -> None: # ``n_tiles`` (host-synced compact tile count) tightens the launch; the # kernel still guards per-tile defensively. wp_device = str(sorted_pos_x.device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_batch_query_cluster_tile_coo( n_tiles=int(n_tiles) if n_tiles > 0 else None, sorted_atom_index=wp.from_torch( sorted_atom_index, dtype=wp.int32, return_ctype=True ), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, return_ctype=True), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, return_ctype=True), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, return_ctype=True), cell_batch=wp.from_torch( cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), inv_cell_batch=wp.from_torch( inv_cell_batch.contiguous(), dtype=wp.mat33f, return_ctype=True, ), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ), tile_system=wp.from_torch(tile_system, dtype=wp.int32, return_ctype=True), cutoff=float(cutoff), natom=int(natom), max_pairs=int(max_pairs), pair_counter=wp.from_torch(pair_counter, dtype=wp.int32, return_ctype=True), coo_list=wp.from_torch(coo_list, dtype=wp.int32, return_ctype=True), coo_shifts=wp.from_torch(coo_shifts, dtype=wp.int32, return_ctype=True), wp_dtype=wp_dtype, device=wp_device, ) @_batch_query_cluster_tile_coo_op.register_fake def _( cutoff: float, natom: int, max_pairs: int, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, ) -> None: return None def _batch_query_cluster_tile_coo_optional( cell_batch: torch.Tensor, inv_cell_batch: torch.Tensor, natom: int, max_pairs: int, cutoff: float, sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, *, n_tiles: int, rebuild_flags: torch.Tensor | None, tile_offsets: torch.Tensor | None, tile_counts: torch.Tensor | None, pair_offsets: torch.Tensor | None, pair_counts: torch.Tensor | None, return_vectors: bool, return_distances: bool, pair_fn: wp.Function | None, pair_params: torch.Tensor | None, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, pair_energies: torch.Tensor | None, pair_forces: torch.Tensor | None, ) -> None: """Pair-output COO path for the batched binding. ``n_tiles`` (compact tile count, or ``<= 0`` for the segmented full-buffer launch) sets the launch dimension; the kernel still guards per-tile. """ wp_device = str(sorted_pos_x.device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_vec_dtype = wp.vec3f wp_pair_params = ( wp.from_torch(pair_params, dtype=wp_dtype) if pair_params is not None else None ) wp_neighbor_vectors = ( wp.from_torch(neighbor_vectors, dtype=wp_vec_dtype) if neighbor_vectors is not None else None ) wp_neighbor_distances = ( wp.from_torch(neighbor_distances, dtype=wp_dtype) if neighbor_distances is not None else None ) wp_pair_energies = ( wp.from_torch(pair_energies, dtype=wp_dtype) if pair_energies is not None else None ) wp_pair_forces = ( wp.from_torch(pair_forces, dtype=wp_vec_dtype) if pair_forces is not None else None ) wp_batch_query_cluster_tile_coo( sorted_atom_index=wp.from_torch(sorted_atom_index, dtype=wp.int32), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype), cell_batch=wp.from_torch(cell_batch.contiguous(), dtype=wp.mat33f), inv_cell_batch=wp.from_torch(inv_cell_batch.contiguous(), dtype=wp.mat33f), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32), tile_row_group=wp.from_torch(tile_row_group, dtype=wp.int32), tile_col_group=wp.from_torch(tile_col_group, dtype=wp.int32), tile_system=wp.from_torch(tile_system, dtype=wp.int32), cutoff=float(cutoff), natom=int(natom), max_pairs=int(max_pairs), pair_counter=wp.from_torch(pair_counter, dtype=wp.int32), coo_list=wp.from_torch(coo_list, dtype=wp.int32), coo_shifts=wp.from_torch(coo_shifts, dtype=wp.int32), wp_dtype=wp_dtype, device=wp_device, n_tiles=int(n_tiles) if n_tiles > 0 else None, rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool) if rebuild_flags is not None else None ), tile_offsets=( wp.from_torch(tile_offsets, dtype=wp.int32) if tile_offsets is not None else None ), tile_counts=( wp.from_torch(tile_counts, dtype=wp.int32) if tile_counts is not None else None ), pair_offsets=( wp.from_torch(pair_offsets, dtype=wp.int32) if pair_offsets is not None else None ), pair_counts=( wp.from_torch(pair_counts, dtype=wp.int32) if pair_counts is not None else None ), return_vectors=bool(return_vectors), return_distances=bool(return_distances), pair_fn=pair_fn, pair_params=wp_pair_params, neighbor_vectors=wp_neighbor_vectors, neighbor_distances=wp_neighbor_distances, pair_energies=wp_pair_energies, pair_forces=wp_pair_forces, )
[docs] def batch_query_cluster_tile_coo( sorted_atom_index: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: torch.Tensor, cell_batch: torch.Tensor, num_tiles: torch.Tensor, tile_row_group: torch.Tensor, tile_col_group: torch.Tensor, tile_system: torch.Tensor, cutoff: float, natom: int, max_pairs: int, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, inv_cell_batch: torch.Tensor | None = None, *, rebuild_flags: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, pair_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, ) -> None: """Convert the batched tile pair list to flat COO pair list in place. Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. Optional pair outputs use flat COO buffers with length ``max_pairs``; they are written in the same order as ``coo_list``. Parameters ---------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation from padded slot to original atom index. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted x-coordinates in the padded SoA layout. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted y-coordinates in the padded SoA layout. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Morton-sorted z-coordinates in the padded SoA layout. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. num_tiles : torch.Tensor, shape (1,), dtype=int32 Scalar tile-pair count from ``batch_build_cluster_tile_list``. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index per tile pair. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index per tile pair. tile_system : torch.Tensor, shape (max_tiles,), dtype=int32 System index per tile pair. cutoff : float Cartesian cutoff radius for pair filtering. natom : int Total number of real atoms across all systems. max_pairs : int Upper bound on the total number of pairs written to ``coo_list``. pair_counter : torch.Tensor, shape (1,), dtype=int32 Output scalar pair count; zeroed then modified in-place. coo_list : torch.Tensor, shape (max_pairs, 2), dtype=int32 Output COO pair list ``[i, j]``; modified in-place. coo_shifts : torch.Tensor, shape (max_pairs, 3), dtype=int32 Output periodic image shift vectors per pair; modified in-place. inv_cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32, optional Pre-computed inverse of ``cell_batch``. Computed internally when None. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=bool, optional Per-system selective rebuild flags. Requires ``tile_offsets``, ``tile_counts``, ``pair_offsets``, and ``pair_counts``. tile_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system tile segment offsets for the segmented path. tile_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Per-system tile counts for the segmented path. pair_offsets : torch.Tensor, shape (num_systems + 1,), dtype=int32, optional Fixed per-system COO segment offsets for the segmented path. pair_counts : torch.Tensor, shape (num_systems,), dtype=int32, optional Output per-system pair counts for the segmented path; modified in-place. return_vectors : bool, default False Write per-pair displacement vectors to ``neighbor_vectors``. return_distances : bool, default False Write per-pair scalar distances to ``neighbor_distances``. pair_fn : wp.Function, optional Module-scope Warp function evaluated for each pair. pair_params : torch.Tensor, optional Per-atom parameters forwarded to ``pair_fn``. neighbor_vectors : torch.Tensor, shape (max_pairs, 3), optional Output displacement vectors; modified in-place when ``return_vectors``. neighbor_distances : torch.Tensor, shape (max_pairs,), optional Output scalar distances; modified in-place when ``return_distances``. pair_energies : torch.Tensor, shape (max_pairs,), optional Output pair energies; modified in-place when ``pair_fn`` is set. pair_forces : torch.Tensor, shape (max_pairs, 3), optional Output pair forces; modified in-place when ``pair_fn`` is set. """ if inv_cell_batch is None: inv_cell_batch = torch.linalg.inv(cell_batch).contiguous() pair_counter.zero_() # Host-sync the tile count: tighten the launch on the compact path and # raise on tile-buffer overflow (missing tiles -> missing pairs). The # segmented path is overflow-checked per system. tile_capacity = int(tile_row_group.shape[0]) if tile_offsets is None: if torch.compiler.is_compiling(): n_tiles = tile_capacity else: n_tiles = int(num_tiles.item()) if n_tiles > tile_capacity: raise NeighborOverflowError(tile_capacity, n_tiles) else: n_tiles = 0 # segmented: full-buffer launch if tile_counts is not None: seg_caps = tile_offsets[1:] - tile_offsets[:-1] overflow = tile_counts > seg_caps if bool(overflow.any().item()): isys = int(torch.nonzero(overflow, as_tuple=False)[0, 0].item()) raise NeighborOverflowError( int(seg_caps[isys].item()), int(tile_counts[isys].item()), system_index=isys, ) needs_direct = ( rebuild_flags is not None or tile_offsets is not None or tile_counts is not None or pair_offsets is not None or pair_counts is not None or _has_partial_or_pair_outputs( 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, ) ) if needs_direct: _batch_query_cluster_tile_coo_optional( cell_batch, inv_cell_batch, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, n_tiles=n_tiles, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, pair_offsets=pair_offsets, pair_counts=pair_counts, 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, ) return _batch_query_cluster_tile_coo_op( cutoff, natom, max_pairs, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, inv_cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, pair_counter, coo_list, coo_shifts, n_tiles, )
# ============================================================================= # High-level convenience # ============================================================================= def _batch_cluster_tile_pair_outputs_forward( positions: torch.Tensor, cell_batch: torch.Tensor, *, batch_ptr: torch.Tensor, cutoff: float, max_neighbors: int, fill_value: int, batch_idx_atom: torch.Tensor, ) -> "_NeighborForwardOutput": """Forward closure for the torch batch_cluster_tile autograd path.""" from nvalchemiops.torch.neighbors._autograd import ( _flatten_active_pairs, _NeighborForwardOutput, ) positions_det = positions.detach() cell_det = cell_batch.detach() device = positions_det.device N = int(batch_ptr[-1].item()) ( sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, ) = allocate_batch_cluster_tile_list( batch_ptr, device, dtype=positions_det.dtype, ) batch_build_cluster_tile_list( positions_det, cutoff, cell_det, batch_ptr, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, ) nm = torch.empty((N, max_neighbors), dtype=torch.int32, device=device) nn = torch.zeros(N, dtype=torch.int32, device=device) nms = torch.empty((N, max_neighbors, 3), dtype=torch.int32, device=device) nv = torch.zeros((N, max_neighbors, 3), dtype=positions_det.dtype, device=device) nd = torch.zeros((N, max_neighbors), dtype=positions_det.dtype, device=device) batch_query_cluster_tile( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_det, num_tiles, tile_row_group, tile_col_group, tile_system, cutoff, N, nm, nn, nms, return_vectors=True, return_distances=True, neighbor_vectors=nv, neighbor_distances=nd, ) if max_neighbors > 0: _batch_cluster_tile_fill_neighbor_matrix_tail_op( nn, nm, int(N), int(max_neighbors), int(fill_value), ) i_idx, j_idx, shifts_flat, batch_idx_flat, mask = _flatten_active_pairs( nm, nn, nms, batch_idx=batch_idx_atom, ) K, M = nm.shape return _NeighborForwardOutput( distances=nd, vectors=nv, extra_outputs=(nm, nn, nms), i_idx_flat=i_idx, j_idx_flat=j_idx, shifts_flat=shifts_flat, batch_idx_flat=batch_idx_flat, active_mask=mask, matrix_shape=(K, M), )
[docs] def batch_cluster_tile_neighbor_list( positions: torch.Tensor, cutoff: float, cell_batch: torch.Tensor, batch_ptr: torch.Tensor, max_neighbors: int | None = None, fill_value: int | None = None, format: str = "matrix", max_pairs: int | None = None, cutoff2: float | None = None, rebuild_flags: torch.Tensor | None = None, # matrix-format outputs neighbor_matrix: torch.Tensor | None = None, neighbor_matrix_shifts: torch.Tensor | None = None, num_neighbors: torch.Tensor | None = None, neighbor_matrix2: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, # coo-format outputs neighbor_list: torch.Tensor | None = None, neighbor_list_shifts: torch.Tensor | None = None, pair_counter: torch.Tensor | None = None, pair_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, inv_cell_batch: torch.Tensor | None = None, # scratch buffers sorted_atom_index: torch.Tensor | None = None, sort_inv: torch.Tensor | None = None, sorted_pos_x: torch.Tensor | None = None, sorted_pos_y: torch.Tensor | None = None, sorted_pos_z: torch.Tensor | None = None, batch_idx_sorted: torch.Tensor | None = None, batch_ptr_padded: torch.Tensor | None = None, group_system: torch.Tensor | None = None, group_ptr: torch.Tensor | None = None, group_ctr_x: torch.Tensor | None = None, group_ctr_y: torch.Tensor | None = None, group_ctr_z: torch.Tensor | None = None, group_ext_x: torch.Tensor | None = None, group_ext_y: torch.Tensor | None = None, group_ext_z: torch.Tensor | None = None, num_tiles: torch.Tensor | None = None, tile_row_group: torch.Tensor | None = None, tile_col_group: torch.Tensor | None = None, tile_system: torch.Tensor | None = None, tile_offsets: torch.Tensor | None = None, tile_counts: torch.Tensor | None = None, # Optional matrix outputs / pair-fn surface return_vectors: bool = False, return_distances: bool = False, pair_fn: wp.Function | None = None, pair_params: torch.Tensor | None = None, neighbor_vectors: torch.Tensor | None = None, neighbor_distances: torch.Tensor | None = None, pair_energies: torch.Tensor | None = None, pair_forces: torch.Tensor | None = None, max_tiles_per_group: int | None = None, ) -> tuple[torch.Tensor, ...]: """Build a batched cluster-pair tile neighbor list (one-shot convenience). Batched PyTorch binding for the cluster-pair tile algorithm. Supports triclinic ``cell_batch`` of shape ``(num_systems, 3, 3)`` and arbitrary per-system atom counts (padded internally to a multiple of ``TILE_GROUP_SIZE``). Cluster-tile is CUDA float32 only. Parameters ---------- positions : torch.Tensor, shape (total_atoms, 3), dtype=float32 Concatenated atomic coordinates across systems. cutoff : float Cutoff distance in Cartesian units. Must be positive. cell_batch : torch.Tensor, shape (num_systems, 3, 3), dtype=float32 Per-system unit cell matrices. Cluster-tile assumes fully periodic boundaries. batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=int32 or int64 CSR pointer separating systems. Assumes positions are laid out in system-contiguous order (system 0 atoms first, then system 1, and so on); interleaved layouts are **not supported** and will silently emit cross-system pairs. When invoked via :func:`nvalchemiops.torch.neighbors.neighbor_list` with a ``batch_idx`` argument, the dispatcher derives ``batch_ptr`` by assuming ``batch_idx`` is sorted by system — the same contract. max_neighbors : int, optional Max neighbors per atom (``"matrix"`` format only). Falls back to :func:`estimate_max_neighbors`. fill_value : int, optional Matrix sentinel; defaults to ``total_atoms``. format : {"matrix", "coo", "tile"}, default "matrix" Output representation. See Returns. max_pairs : int, optional Upper bound for COO output; defaults to ``total_atoms * max_neighbors``. cutoff2 : float, optional Secondary cutoff for matrix output. Dual cutoff is matrix-only and cannot be combined with pair-output buffers. rebuild_flags : torch.Tensor, shape (num_systems,), dtype=torch.bool, optional Per-system selective rebuild flags. Supported for matrix output and segmented COO output. tile_offsets, tile_counts : torch.Tensor, optional Fixed per-system tile offsets and OUTPUT tile counters for segmented tile-list state. Use ``estimate_batch_cluster_tile_segments`` to size these arrays. pair_offsets, pair_counts : torch.Tensor, optional Fixed per-system COO offsets and OUTPUT pair counters for segmented COO output. Compact COO cannot be combined with ``rebuild_flags``. neighbor_matrix, num_neighbors, neighbor_matrix_shifts : torch.Tensor, optional Pre-allocated matrix-format outputs. All-or-nothing across the trio. neighbor_list, neighbor_list_shifts, pair_counter : torch.Tensor, optional Pre-allocated COO-format outputs. Shapes ``(2, max_pairs)``, ``(max_pairs, 3)``, ``(1,)`` int32. All-or-nothing across the trio. inv_cell_batch : torch.Tensor, optional Pre-computed inverse cell matrices. sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_*, group_ext_*, num_tiles, tile_row_group, tile_col_group, tile_system : torch.Tensor, optional Pre-allocated scratch buffers (shapes as returned by ``allocate_batch_cluster_tile_list``). All-or-nothing: provide every scratch tensor or none. The trigger is ``sorted_atom_index``. return_vectors, return_distances : bool, default False Write per-pair displacements / scalar distances to ``neighbor_vectors`` / ``neighbor_distances``. Matrix format uses ``(total_atoms, max_neighbors, ...)`` buffers; COO format uses flat ``(max_pairs, ...)`` buffers. pair_fn : wp.Function, optional Module-scope Warp ``@wp.func`` of signature ``(r_ij, distance, pair_params, i, j) -> (energy, force)``. pair_params : torch.Tensor, optional Per-atom pair-function parameters; required with ``pair_fn``. neighbor_vectors, neighbor_distances, pair_energies, pair_forces : torch.Tensor, optional OUTPUT buffers, written only when the corresponding enable flag / ``pair_fn`` is active. max_tiles_per_group : int, optional Upper bound on neighbor groups per row group for scratch allocation. Passing this skips the geometry-aware sizing preflight, which otherwise synchronizes per-system counts and cell volumes to the host. Returns ------- tuple of torch.Tensor Shape depends on ``format``: - ``"matrix"`` (default): ``(neighbor_matrix, num_neighbors, neighbor_matrix_shifts)``, with optional ``(*, distances)`` and/or ``(*, vectors)`` appended when ``return_distances`` / ``return_vectors`` is True, and optional ``(*, pair_energies, pair_forces)`` when ``pair_fn`` is set. - ``"coo"``: ``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` via the direct ``batch_query_cluster_tile_coo`` path (no matrix intermediate). ``neighbor_ptr`` is reconstructed from ``bincount(neighbor_list[0])``. With segmented COO, returns ``(neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts)`` without trimming the caller-owned fixed segments. - ``"tile"``: 11-tuple ``(num_tiles, tile_row_group, tile_col_group, tile_system, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_ptr)`` --- per-tile and per-system mapping arrays a batch tile consumer needs. Notes ----- - Cluster-tile is CUDA float32 only; float64 ``positions`` is rejected. - Cluster-tile does not support partial neighbor lists (no ``target_indices`` kwarg). - The unified :func:`nvalchemiops.torch.neighbors.neighbor_list` entry point may select this binding automatically when the selector guards and cost model prefer it; pass ``method="batch_cluster_tile"`` to force it. See Also -------- nvalchemiops.torch.neighbors.cluster_tile_neighbor_list : Single-system companion entry point. nvalchemiops.torch.neighbors.batch_cluster_tile.batch_build_cluster_tile_list : Lower-level build step exposed for caching across queries. nvalchemiops.torch.neighbors.batch_cluster_tile.batch_query_cluster_tile : Lower-level query step. """ if positions.dtype != torch.float32: raise TypeError("positions must be float32") if format not in ("matrix", "coo", "tile"): raise ValueError( f"format must be 'matrix' | 'coo' | 'tile'; got {format!r}", ) has_pair_outputs = ( bool(return_vectors) or bool(return_distances) or pair_fn is not None or pair_params is not None or neighbor_vectors is not None or neighbor_distances is not None or pair_energies is not None or pair_forces is not None ) if has_pair_outputs and format == "tile": raise NotImplementedError( "Pair outputs (return_vectors / return_distances / pair_fn) " "are not supported with format='tile'. Use format='matrix' " "or format='coo'.", ) if cutoff2 is not None: if format != "matrix": raise ValueError( "cluster_tile cutoff2 is supported only with format='matrix'" ) if has_pair_outputs: raise ValueError( "cluster_tile cutoff2 cannot be combined with pair outputs" ) if rebuild_flags is not None and format == "tile": raise ValueError( "cluster_tile selective rebuild is not supported with format='tile'" ) if ( rebuild_flags is not None and format == "coo" and (pair_offsets is None or pair_counts is None) ): raise ValueError( "cluster_tile selective COO requires pair_offsets and pair_counts" ) if (pair_offsets is None) != (pair_counts is None): raise ValueError("Pass both 'pair_offsets' and 'pair_counts', or neither.") if (tile_offsets is None) != (tile_counts is None): raise ValueError("Pass both 'tile_offsets' and 'tile_counts', or neither.") if batch_ptr.shape[0] < 2: raise ValueError("batch_ptr must have length at least 2") device = positions.device N = int(batch_ptr[-1].item()) if max_neighbors is None: max_neighbors = max( estimate_max_neighbors(cutoff2 if cutoff2 is not None else cutoff), TILE_GROUP_SIZE, ) if fill_value is None: fill_value = N needs_segmented_tiles = rebuild_flags is not None or pair_offsets is not None if needs_segmented_tiles and tile_offsets is None: _tile_caps, tile_offsets, _pair_caps, default_pair_offsets = ( estimate_batch_cluster_tile_segments(batch_ptr, int(max_neighbors)) ) tile_counts = torch.empty( int(tile_offsets.shape[0]) - 1, dtype=torch.int32, device=device ) if format == "coo" and pair_offsets is None: pair_offsets = default_pair_offsets pair_counts = torch.empty( int(pair_offsets.shape[0]) - 1, dtype=torch.int32, device=device ) elif needs_segmented_tiles and tile_counts is None: raise ValueError("tile_counts is required when tile_offsets is provided") # Autograd routing: distances/vectors only (no pair_fn). if ( (bool(return_distances) or bool(return_vectors)) and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None and format == "matrix" and rebuild_flags is None ): from nvalchemiops.torch.neighbors._autograd import _route_pair_outputs # Build atom -> system mapping for the backward (per-system cell grad). per_sys_counts = batch_ptr[1:] - batch_ptr[:-1] batch_idx_atom = torch.repeat_interleave( torch.arange(per_sys_counts.shape[0], dtype=torch.int32, device=device), per_sys_counts.to(torch.int64), ) forward_kwargs = { "batch_ptr": batch_ptr, "cutoff": cutoff, "max_neighbors": int(max_neighbors), "fill_value": int(fill_value), "batch_idx_atom": batch_idx_atom, } distances_out, vectors_out, nm_out, nn_out, shifts_out = _route_pair_outputs( positions, cell_batch, _batch_cluster_tile_pair_outputs_forward, forward_kwargs, ) base = (nm_out, nn_out, shifts_out) if return_distances and return_vectors: return (*base, distances_out, vectors_out) if return_distances: return (*base, distances_out) return (*base, vectors_out) # Tile candidates must cover the larger radius so the cutoff2 matrix cannot # miss pairs in the (cutoff, cutoff2] shell; the query filters each matrix # by its own cutoff. build_cutoff = cutoff if cutoff2 is None else max(float(cutoff), float(cutoff2)) if sorted_atom_index is None: if max_tiles_per_group is None: max_tiles_per_group = estimate_batch_max_tiles_per_group( batch_ptr, build_cutoff, cell_batch ) ( sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, ) = allocate_batch_cluster_tile_list( batch_ptr, device, dtype=positions.dtype, max_tiles_per_group=max_tiles_per_group, ) batch_build_cluster_tile_list( positions, build_cutoff, cell_batch, batch_ptr, sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, tile_system, inv_cell_batch=inv_cell_batch, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, ) if format == "tile": # Raw-tile callers must not receive a silently truncated tile list # (format='tile' is always the compact, non-selective path). n_tiles = int(num_tiles.item()) tile_capacity = int(tile_row_group.shape[0]) if n_tiles > tile_capacity: raise NeighborOverflowError(tile_capacity, n_tiles) return ( num_tiles, tile_row_group, tile_col_group, tile_system, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded, group_ptr, ) if format == "coo": segmented_coo = pair_offsets is not None if segmented_coo: max_pairs = int(pair_offsets[-1].item()) elif max_pairs is None: max_pairs = N * max_neighbors if neighbor_list is None: coo_buf = torch.empty( (max_pairs, 2), dtype=torch.int32, device=device, ) else: coo_buf = neighbor_list.transpose(0, 1) if neighbor_list_shifts is None: neighbor_list_shifts = torch.empty( (max_pairs, 3), dtype=torch.int32, device=device, ) if pair_counter is None: pair_counter = torch.zeros(1, dtype=torch.int32, device=device) if segmented_coo and pair_counts is None: pair_counts = torch.empty( int(pair_offsets.shape[0]) - 1, dtype=torch.int32, device=device ) batch_query_cluster_tile_coo( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, cutoff, N, int(max_pairs), pair_counter, coo_buf, neighbor_list_shifts, inv_cell_batch=inv_cell_batch, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, pair_offsets=pair_offsets, pair_counts=pair_counts, 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, ) if segmented_coo: segment_caps = pair_offsets[1:] - pair_offsets[:-1] overflow = pair_counts > segment_caps if bool(overflow.any().item()): isys = int(torch.nonzero(overflow, as_tuple=False)[0, 0].item()) raise NeighborOverflowError( int(segment_caps[isys].item()), int(pair_counts[isys].item()), system_index=isys, ) return ( coo_buf.transpose(0, 1).contiguous(), pair_offsets, pair_counts, neighbor_list_shifts, ) npairs = int(pair_counter.item()) if npairs > int(max_pairs): raise NeighborOverflowError(int(max_pairs), npairs) nl = coo_buf[:npairs].transpose(0, 1).contiguous() nls = neighbor_list_shifts[:npairs].contiguous() per_atom_counts = torch.bincount(nl[0].long(), minlength=N).to( torch.int32, ) neighbor_ptr = torch.cat( [ torch.zeros(1, dtype=torch.int32, device=device), torch.cumsum(per_atom_counts, dim=0).to(torch.int32), ], ) return nl, neighbor_ptr, nls # ``format == "matrix"``: skip-prefill matrix path with tail fill. if neighbor_matrix is None: neighbor_matrix = torch.empty( (N, max_neighbors), dtype=torch.int32, device=device, ) if num_neighbors is None: num_neighbors = torch.zeros(N, dtype=torch.int32, device=device) elif rebuild_flags is None: num_neighbors.zero_() if neighbor_matrix_shifts is None: neighbor_matrix_shifts = torch.empty( (N, max_neighbors, 3), dtype=torch.int32, device=device, ) if cutoff2 is not None: if neighbor_matrix2 is None: neighbor_matrix2 = torch.empty( (N, max_neighbors), dtype=torch.int32, device=device ) if num_neighbors2 is None: num_neighbors2 = torch.zeros(N, dtype=torch.int32, device=device) elif rebuild_flags is None: num_neighbors2.zero_() if neighbor_matrix_shifts2 is None: neighbor_matrix_shifts2 = torch.empty( (N, max_neighbors, 3), dtype=torch.int32, device=device ) # Allocate pair output buffers when caller omits them. if return_vectors and neighbor_vectors is None: neighbor_vectors = torch.empty( (N, max_neighbors, 3), dtype=positions.dtype, device=device, ) if return_distances and neighbor_distances is None: neighbor_distances = torch.empty( (N, max_neighbors), dtype=positions.dtype, device=device, ) if pair_fn is not None: if pair_energies is None: pair_energies = torch.empty( (N, max_neighbors), dtype=positions.dtype, device=device, ) if pair_forces is None: pair_forces = torch.empty( (N, max_neighbors, 3), dtype=positions.dtype, device=device, ) batch_query_cluster_tile( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_batch, num_tiles, tile_row_group, tile_col_group, tile_system, cutoff, N, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, inv_cell_batch=inv_cell_batch, cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, tile_offsets=tile_offsets, tile_counts=tile_counts, batch_idx=torch.repeat_interleave( torch.arange(int(batch_ptr.shape[0]) - 1, dtype=torch.int32, device=device), (batch_ptr[1:] - batch_ptr[:-1]).to(torch.int64), ) if rebuild_flags is not None else None, 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, ) max_seen = int(num_neighbors.max().item()) if N > 0 else 0 if max_seen > int(max_neighbors): raise NeighborOverflowError(int(max_neighbors), max_seen) if cutoff2 is not None and num_neighbors2 is not None: max_seen2 = int(num_neighbors2.max().item()) if N > 0 else 0 if max_seen2 > int(max_neighbors): raise NeighborOverflowError(int(max_neighbors), max_seen2) if max_neighbors > 0: _batch_cluster_tile_fill_neighbor_matrix_tail_op( num_neighbors, neighbor_matrix, int(N), int(max_neighbors), int(fill_value), ) if cutoff2 is not None: _batch_cluster_tile_fill_neighbor_matrix_tail_op( num_neighbors2, neighbor_matrix2, int(N), int(max_neighbors), int(fill_value), ) if cutoff2 is not None: return ( neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, ) return neighbor_matrix, num_neighbors, neighbor_matrix_shifts