Source code for nvalchemiops.torch.neighbors.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 single-system cluster-pair tile neighbor list.

Mirrors the ``cell_list`` / ``naive`` wrapper pattern: exposes low-level
component ops (``build_cluster_tile_list``, ``query_cluster_tile``,
``query_cluster_tile_coo``) that fill pre-allocated tensors, plus a high-level
convenience entry point ``cluster_tile_neighbor_list``.  All torch-side work
(Morton sort, allocation, ``wp.from_torch`` conversion) lives in this
module; the Warp layer at ``nvalchemiops.neighbors.cluster_tile`` only
sees ``wp.array`` inputs.

Scope: single system, orthorhombic or triclinic PBC, float32, any
``N >= 0`` (the wrapper pads internally to a multiple of TILE_GROUP_SIZE).
"""

import torch
import warp as wp

from nvalchemiops.neighbors.cluster_tile import (
    TILE_GROUP_SIZE,
    estimate_max_tiles_per_group,
)
from nvalchemiops.neighbors.cluster_tile import (
    build_cluster_tile_list as wp_build_cluster_tile_list,
)
from nvalchemiops.neighbors.cluster_tile import (
    query_cluster_tile as wp_query_cluster_tile,
)
from nvalchemiops.neighbors.cluster_tile import (
    query_cluster_tile_coo as wp_query_cluster_tile_coo,
)
from nvalchemiops.neighbors.cluster_tile.kernels import (
    _get_query_cluster_tile_direct_csr_count_kernel,
    _get_query_cluster_tile_direct_csr_fill_kernel,
)
from nvalchemiops.neighbors.neighbor_utils import (
    _selective_fill_neighbor_matrix_tail as wp_selective_fill_neighbor_matrix_tail,
)
from nvalchemiops.neighbors.neighbor_utils import 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_single as wp_selective_zero_num_neighbors_single,
)
from nvalchemiops.neighbors.output_args import (
    _has_partial_or_pair_outputs,
    _prepare_coo_pair_output_args,
)
from nvalchemiops.torch._warp_op_helpers import (
    _capture_safe_inverse,
    scoped_torch_warp_stream,
)
from nvalchemiops.torch.neighbors._autograd import (
    _reconstruct_coo_geometry,
    _reconstruct_matrix_geometry,
)
from nvalchemiops.torch.neighbors._fixed_cell import _FixedCellGeometry
from nvalchemiops.torch.neighbors.neighbor_utils import (
    _check_neighbor_capacity,
    _check_tile_buffer_capacity,
    _compact_coo_prefix,
    _normalize_compiled_single_segment_coo_count,
    _prepare_compact_coo_geometry_buffers,
    _validate_cluster_tile_matrix_outputs,
    _validate_compact_coo_outputs,
    _validate_segmented_coo_state,
)
from nvalchemiops.torch.types import get_wp_dtype

__all__ = [
    "TILE_GROUP_SIZE",
    "estimate_cluster_tile_list_sizes",
    "allocate_cluster_tile_list",
    "build_cluster_tile_list",
    "query_cluster_tile",
    "query_cluster_tile_coo",
    "cluster_tile_neighbor_list",
]


def _wp_fixed_cluster_geometry(
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> dict[str, object]:
    """Convert optional prepared cluster geometry arrays for a Warp launcher."""
    return {
        "fixed_cell": fixed_cell,
        "qr": wp.from_torch(
            qr, dtype=wp.float32, requires_grad=False, return_ctype=True
        )
        if qr is not None
        else None,
        "axis_aligned": wp.from_torch(
            axis_aligned, dtype=wp.bool, requires_grad=False, return_ctype=True
        )
        if axis_aligned is not None
        else None,
        "fractional_rounding_certified": wp.from_torch(
            fractional_rounding_certified,
            dtype=wp.bool,
            requires_grad=False,
            return_ctype=True,
        )
        if fractional_rounding_certified is not None
        else None,
        "qr_height_certified": wp.from_torch(
            qr_height_certified, dtype=wp.bool, requires_grad=False, return_ctype=True
        )
        if qr_height_certified is not None
        else None,
        "bbox_cutoff_bounds": wp.from_torch(
            bbox_cutoff_bounds, dtype=wp.float32, requires_grad=False, return_ctype=True
        )
        if bbox_cutoff_bounds is not None
        else None,
    }


def _fixed_cluster_op_args(
    geometry: _FixedCellGeometry | None,
) -> tuple[
    bool,
    torch.Tensor | None,
    torch.Tensor | None,
    torch.Tensor | None,
    torch.Tensor | None,
    torch.Tensor | None,
]:
    """Return static fixed mode and readonly geometry tensors for custom ops."""
    if geometry is None:
        return False, None, None, None, None, None
    return (
        True,
        geometry.qr,
        geometry.axis_aligned,
        geometry.fractional_rounding_certified,
        geometry.qr_height_certified,
        geometry.bbox_cutoff_bounds,
    )


def _fixed_cluster_geometry_record(
    inv_cell: torch.Tensor,
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> _FixedCellGeometry | None:
    """Wrap query-op tensors in the private geometry record expected downstream."""
    if not fixed_cell:
        return None
    return _FixedCellGeometry(
        inv_cell=inv_cell,
        cell=None,
        cells_per_dimension=None,
        neighbor_search_radius=None,
        cells_per_system=None,
        cell_offsets=None,
        qr=qr,
        axis_aligned=axis_aligned,
        fractional_rounding_certified=fractional_rounding_certified,
        qr_height_certified=qr_height_certified,
        bbox_cutoff_bounds=bbox_cutoff_bounds,
    )


def _wp_direct_csr_geometry_inputs(
    device: torch.device,
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> tuple[wp.array, wp.array, wp.array, wp.array, wp.array]:
    """Convert direct-CSR geometry inputs, using empty dynamic sentinels."""
    device_str = str(device)
    return (
        wp.from_torch(qr, dtype=wp.float32, requires_grad=False)
        if qr is not None
        else wp.empty((0, 15), dtype=wp.float32, device=device_str),
        wp.from_torch(axis_aligned, dtype=wp.bool, requires_grad=False)
        if axis_aligned is not None
        else wp.empty(0, dtype=wp.bool, device=device_str),
        wp.from_torch(fractional_rounding_certified, dtype=wp.bool, requires_grad=False)
        if fractional_rounding_certified is not None
        else wp.empty(0, dtype=wp.bool, device=device_str),
        wp.from_torch(qr_height_certified, dtype=wp.bool, requires_grad=False)
        if qr_height_certified is not None
        else wp.empty(0, dtype=wp.bool, device=device_str),
        wp.from_torch(bbox_cutoff_bounds, dtype=wp.float32, requires_grad=False)
        if bbox_cutoff_bounds is not None
        else wp.empty((0, 3), dtype=wp.float32, device=device_str),
    )


@torch.library.custom_op(
    "nvalchemiops::_cluster_tile_direct_csr_count",
    mutates_args=("row_counts",),
)
@scoped_torch_warp_stream
def _cluster_tile_direct_csr_count_op(
    cutoff: float,
    natom: int,
    cell: torch.Tensor,
    inv_cell: torch.Tensor,
    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,
    row_counts: torch.Tensor,
    launch_tiles: int,
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> None:
    geometry = _wp_direct_csr_geometry_inputs(
        sorted_pos_x.device,
        fixed_cell,
        qr,
        axis_aligned,
        fractional_rounding_certified,
        qr_height_certified,
        bbox_cutoff_bounds,
    )
    wp.launch_tiled(
        _get_query_cluster_tile_direct_csr_count_kernel(
            batched=False, fixed_cell=fixed_cell
        ),
        dim=[launch_tiles],
        inputs=[
            wp.from_torch(x, dtype=d, return_ctype=True)
            for x, d in (
                (sorted_pos_x, wp.float32),
                (sorted_pos_y, wp.float32),
                (sorted_pos_z, wp.float32),
                (sorted_atom_index, wp.int32),
                (cell.unsqueeze(0), wp.mat33f),
                (inv_cell.unsqueeze(0), wp.mat33f),
            )
        ]
        + list(geometry)
        + [
            float(cutoff * cutoff),
            int(natom),
            wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_col_group, dtype=wp.int32, return_ctype=True),
            wp.empty(1, dtype=wp.int32, device=str(sorted_pos_x.device)),
            wp.from_torch(row_counts, dtype=wp.int32, return_ctype=True),
        ],
        device=str(sorted_pos_x.device),
        block_dim=TILE_GROUP_SIZE,
    )


@_cluster_tile_direct_csr_count_op.register_fake
def _(*args, **kwargs) -> None:
    return None


@torch.library.custom_op(
    "nvalchemiops::_cluster_tile_direct_csr_fill",
    mutates_args=(
        "cursors",
        "coo_list",
        "coo_shifts",
        "neighbor_vectors",
        "neighbor_distances",
    ),
)
@scoped_torch_warp_stream
def _cluster_tile_direct_csr_fill_op(
    cutoff: float,
    natom: int,
    cell: torch.Tensor,
    inv_cell: torch.Tensor,
    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,
    cursors: torch.Tensor,
    coo_list: torch.Tensor,
    coo_shifts: torch.Tensor,
    neighbor_vectors: torch.Tensor,
    neighbor_distances: torch.Tensor,
    return_vectors: bool,
    return_distances: bool,
    max_pairs: int,
    launch_tiles: int,
    fixed_cell: bool,
    qr: torch.Tensor | None,
    axis_aligned: torch.Tensor | None,
    fractional_rounding_certified: torch.Tensor | None,
    qr_height_certified: torch.Tensor | None,
    bbox_cutoff_bounds: torch.Tensor | None,
) -> None:
    geometry = _wp_direct_csr_geometry_inputs(
        sorted_pos_x.device,
        fixed_cell,
        qr,
        axis_aligned,
        fractional_rounding_certified,
        qr_height_certified,
        bbox_cutoff_bounds,
    )
    wp.launch_tiled(
        _get_query_cluster_tile_direct_csr_fill_kernel(
            batched=False,
            fixed_cell=fixed_cell,
            return_vectors=return_vectors,
            return_distances=return_distances,
        ),
        dim=[launch_tiles],
        inputs=[
            wp.from_torch(x, dtype=d, return_ctype=True)
            for x, d in (
                (sorted_pos_x, wp.float32),
                (sorted_pos_y, wp.float32),
                (sorted_pos_z, wp.float32),
                (sorted_atom_index, wp.int32),
                (cell.unsqueeze(0), wp.mat33f),
                (inv_cell.unsqueeze(0), wp.mat33f),
            )
        ]
        + list(geometry)
        + [
            float(cutoff * cutoff),
            int(natom),
            int(max_pairs),
            wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True),
            wp.from_torch(tile_col_group, dtype=wp.int32, return_ctype=True),
            wp.empty(1, dtype=wp.int32, device=str(sorted_pos_x.device)),
            wp.from_torch(cursors, dtype=wp.int32, return_ctype=True),
            wp.from_torch(coo_list, dtype=wp.int32, return_ctype=True),
            wp.from_torch(coo_shifts, dtype=wp.int32, return_ctype=True),
            wp.from_torch(neighbor_vectors, dtype=wp.vec3f, return_ctype=True),
            wp.from_torch(neighbor_distances, dtype=wp.float32, return_ctype=True),
            wp.empty((1, 1), dtype=wp.float32, device=str(sorted_pos_x.device)),
            wp.empty(1, dtype=wp.float32, device=str(sorted_pos_x.device)),
            wp.empty(1, dtype=wp.vec3f, device=str(sorted_pos_x.device)),
        ],
        device=str(sorted_pos_x.device),
        block_dim=TILE_GROUP_SIZE,
    )


@_cluster_tile_direct_csr_fill_op.register_fake
def _(*args, **kwargs) -> None:
    return None


@scoped_torch_warp_stream
def _cluster_tile_direct_csr_fill_pair_outputs(
    cutoff: float,
    natom: int,
    cell: torch.Tensor,
    inv_cell: torch.Tensor,
    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,
    cursors: torch.Tensor,
    coo_list: torch.Tensor,
    coo_shifts: torch.Tensor,
    pair_fn: wp.Function,
    pair_params: torch.Tensor,
    pair_energies: torch.Tensor,
    pair_forces: torch.Tensor,
    neighbor_vectors: torch.Tensor | None,
    neighbor_distances: torch.Tensor | None,
    return_vectors: bool,
    return_distances: bool,
    max_pairs: int,
    launch_tiles: int,
    _fixed_cell_geometry: _FixedCellGeometry | None = None,
) -> None:
    """Fill compact CSR rows and eager raw pair-function outputs together."""
    device = str(sorted_pos_x.device)
    wp_pair_params = wp.from_torch(pair_params, dtype=wp.float32)
    wp_neighbor_vectors = (
        wp.from_torch(neighbor_vectors, dtype=wp.vec3f)
        if neighbor_vectors is not None
        else None
    )
    wp_neighbor_distances = (
        wp.from_torch(neighbor_distances, dtype=wp.float32)
        if neighbor_distances is not None
        else None
    )
    (
        vectors_arg,
        distances_arg,
        pair_params_arg,
        energies_arg,
        forces_arg,
    ) = _prepare_coo_pair_output_args(
        wp.float32,
        device,
        max_pairs,
        return_vectors=return_vectors,
        return_distances=return_distances,
        pair_fn=pair_fn,
        pair_params=wp_pair_params,
        neighbor_vectors=wp_neighbor_vectors,
        neighbor_distances=wp_neighbor_distances,
        pair_energies=wp.from_torch(pair_energies, dtype=wp.float32),
        pair_forces=wp.from_torch(pair_forces, dtype=wp.vec3f),
    )
    wp.launch_tiled(
        _get_query_cluster_tile_direct_csr_fill_kernel(
            batched=False,
            fixed_cell=_fixed_cell_geometry is not None,
            return_vectors=return_vectors,
            return_distances=return_distances,
            pair_fn=pair_fn,
        ),
        dim=[launch_tiles],
        inputs=[
            wp.from_torch(sorted_pos_x, dtype=wp.float32),
            wp.from_torch(sorted_pos_y, dtype=wp.float32),
            wp.from_torch(sorted_pos_z, dtype=wp.float32),
            wp.from_torch(sorted_atom_index, dtype=wp.int32),
            wp.from_torch(cell.unsqueeze(0), dtype=wp.mat33f),
            wp.from_torch(inv_cell.unsqueeze(0), dtype=wp.mat33f),
            *_wp_direct_csr_geometry_inputs(
                sorted_pos_x.device,
                *_fixed_cluster_op_args(_fixed_cell_geometry),
            ),
            float(cutoff * cutoff),
            int(natom),
            int(max_pairs),
            wp.from_torch(num_tiles, dtype=wp.int32),
            wp.from_torch(tile_row_group, dtype=wp.int32),
            wp.from_torch(tile_col_group, dtype=wp.int32),
            wp.empty(1, dtype=wp.int32, device=device),
            wp.from_torch(cursors, dtype=wp.int32),
            wp.from_torch(coo_list, dtype=wp.int32),
            wp.from_torch(coo_shifts, dtype=wp.int32),
            vectors_arg,
            distances_arg,
            pair_params_arg,
            energies_arg,
            forces_arg,
        ],
        device=device,
        block_dim=TILE_GROUP_SIZE,
    )


@torch.library.custom_op(
    "nvalchemiops::_cluster_tile_fill_neighbor_matrix_tail",
    mutates_args=("neighbor_matrix",),
)
@scoped_torch_warp_stream
def _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),
    )


@_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


@torch.library.custom_op(
    "nvalchemiops::_cluster_tile_selective_zero_num_neighbors_single",
    mutates_args=("num_neighbors",),
)
@scoped_torch_warp_stream
def _cluster_tile_selective_zero_num_neighbors_single_op(
    num_neighbors: torch.Tensor,
    rebuild_flags: torch.Tensor,
) -> None:
    wp_selective_zero_num_neighbors_single(
        wp.from_torch(
            num_neighbors, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        wp.from_torch(
            rebuild_flags, dtype=wp.bool, requires_grad=False, return_ctype=True
        ),
        str(num_neighbors.device),
    )


@_cluster_tile_selective_zero_num_neighbors_single_op.register_fake
def _(
    num_neighbors: torch.Tensor,
    rebuild_flags: torch.Tensor,
) -> None:
    return None


@torch.library.custom_op(
    "nvalchemiops::_cluster_tile_selective_fill_neighbor_matrix_tail",
    mutates_args=("neighbor_matrix",),
)
@scoped_torch_warp_stream
def _cluster_tile_selective_fill_neighbor_matrix_tail_op(
    num_neighbors: torch.Tensor,
    rebuild_flags: torch.Tensor,
    neighbor_matrix: torch.Tensor,
    max_neighbors: int,
    fill_value: int,
) -> None:
    if max_neighbors <= 0:
        return
    wp_selective_fill_neighbor_matrix_tail(
        wp.from_torch(
            num_neighbors, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        None,
        wp.from_torch(
            rebuild_flags, dtype=wp.bool, requires_grad=False, return_ctype=True
        ),
        int(num_neighbors.shape[0]),
        int(max_neighbors),
        int(fill_value),
        wp.from_torch(
            neighbor_matrix, dtype=wp.int32, requires_grad=False, return_ctype=True
        ),
        str(neighbor_matrix.device),
        batched=False,
    )


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


# =============================================================================
# Sizing + allocation helpers (torch-side, not ``custom_op``-wrapped)
# =============================================================================
[docs] def estimate_cluster_tile_list_sizes( total_atoms: int, max_tiles_per_group: int = 256, ) -> tuple[int, int, int, int]: """Estimate allocation sizes for the tile neighbor list state. Any ``total_atoms >= 0`` is accepted. Nonempty inputs use ``n_padded = ceil(total_atoms / TILE_GROUP_SIZE) * TILE_GROUP_SIZE`` so the kernels see a 32-aligned layout; empty input reserves one tile group. Padding slots receive a sentinel maximum Morton code (see ``_compute_morton_kernel``), sort to the end, and are dropped by the convert/COO kernels' ``i_sorted < natom`` filter. Parameters ---------- total_atoms : int Real atom count. max_tiles_per_group : int, default 256 Sets the capacity of the tile-pair buffer shared by all row groups. The allocated group count is ``ngroup = max(1, ceil(total_atoms / 32))``. The capacity is ``ngroup * min(ngroup, max_tiles_per_group)`` entries. Returns ------- n_padded : int Padded atom count. This is at least ``TILE_GROUP_SIZE``; nonempty inputs are rounded up to a multiple of ``TILE_GROUP_SIZE``. ngroup : int Number of 32-atom groups: ``n_padded // TILE_GROUP_SIZE``. ngroup_padded : int Group-array pad length for in-bounds ``wp.tile_load`` at any TILE-aligned offset. Multiple of ``TILE_GROUP_SIZE``; at least one TILE slack over ``ngroup``. max_tiles : int Allocated tile-pair list capacity. """ if total_atoms < 0: raise ValueError(f"total_atoms must be >= 0; got {total_atoms}") n_padded = ( (total_atoms + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE if n_padded == 0: n_padded = TILE_GROUP_SIZE # always reserve at least one tile ngroup = n_padded // TILE_GROUP_SIZE ngroup_padded = ( (ngroup + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE if ngroup_padded == ngroup: ngroup_padded = ngroup + TILE_GROUP_SIZE max_tiles = ngroup * min(ngroup, max_tiles_per_group) return n_padded, ngroup, ngroup_padded, max_tiles
[docs] def allocate_cluster_tile_list( total_atoms: int, 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, ]: """Allocate all state tensors consumed by ``build_cluster_tile_list``. Sizes each buffer according to the padded atom count and group count returned by :func:`nvalchemiops.torch.neighbors.cluster_tile.estimate_cluster_tile_list_sizes`. Parameters ---------- total_atoms : int Real atom count. Any value ``>= 0`` is accepted. device : torch.device Target device for all allocated tensors. dtype : torch.dtype, optional Floating-point dtype for position and bounding-box arrays. Default is ``torch.float32``. max_tiles_per_group : int, optional Capacity factor for the intermediate tile-pair buffer. For ``g`` row groups, the buffer holds ``g * min(g, max_tiles_per_group)`` tile pairs. Increasing the value up to ``g`` uses more memory and accommodates more candidate tile pairs. Defaults to 256. See :ref:`cluster-tile-buffer-capacity` for sizing details. Returns ------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation that maps sorted rank to original atom index. morton_codes : torch.Tensor, shape (n_padded,), dtype=int32 30-bit Morton codes for each atom in sorted order; padding slots carry a sentinel value ``0x40000000``. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=dtype x-coordinates in Morton-sorted order. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=dtype y-coordinates in Morton-sorted order. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=dtype z-coordinates in Morton-sorted order. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=dtype x-component of each group bounding-box centre. group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=dtype y-component of each group bounding-box centre. group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=dtype z-component of each group bounding-box centre. group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=dtype x half-extent of each group bounding box. group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=dtype y half-extent of each group bounding box. group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=dtype z half-extent of each group bounding box. num_tiles : torch.Tensor, shape (1,), dtype=int32 Atomic counter holding the number of emitted tile pairs. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group index for each emitted tile pair. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group index for each emitted tile pair. See Also -------- :func:`nvalchemiops.torch.neighbors.cluster_tile.estimate_cluster_tile_list_sizes` : Returns the sizing integers used here. :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list` : Fills the allocated buffers. """ n_padded, ngroup, ngroup_padded, max_tiles = estimate_cluster_tile_list_sizes( total_atoms, max_tiles_per_group=max_tiles_per_group, ) # Scratch arrays sized at the padded layout so non-32-aligned # ``total_atoms`` is handled inside ``_build_cluster_tile_list_op``. # Padding slots are populated with sentinel Morton codes (see # ``_compute_morton_kernel``) and dropped by the convert/coo # kernels' ``i_sorted < natom`` filter. sorted_atom_index = torch.empty(n_padded, dtype=torch.int32, device=device) morton_codes = torch.empty(n_padded, 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) 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) return ( sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, )
# ============================================================================= # Internal helpers # ============================================================================= def _cell_from_cell(cell: torch.Tensor) -> torch.Tensor: """Normalize a single-system cell to one contiguous ``(3, 3)`` tensor.""" if cell.ndim == 3: if cell.shape[0] != 1: raise ValueError( f"single-system cluster_tile expects (1, 3, 3) cell; got {tuple(cell.shape)}" ) cell_mat = cell[0] elif cell.ndim == 2: if cell.shape != (3, 3): raise ValueError( f"cell must be (3, 3) or (1, 3, 3); got {tuple(cell.shape)}" ) cell_mat = cell else: raise ValueError(f"cell must be (3, 3) or (1, 3, 3); got {tuple(cell.shape)}") return cell_mat.contiguous() def _cell_invcell_from_cell( cell: torch.Tensor, ) -> tuple[torch.Tensor, torch.Tensor]: """Normalize a cell and compute its inverse with capture-safe checking.""" cell_mat = _cell_from_cell(cell) inv_cell_mat = _capture_safe_inverse(cell_mat).contiguous() return cell_mat, inv_cell_mat def _cell_volume(cell: torch.Tensor) -> float: """Return ``abs(det(cell))`` for a ``(3, 3)`` or ``(1, 3, 3)`` cell.""" cell_mat = cell[0] if cell.ndim == 3 else cell return float(torch.linalg.det(cell_mat.to(torch.float64)).abs().item()) @scoped_torch_warp_stream def _mat33f_from_torch(mat: torch.Tensor): """Zero-copy view a ``(1, 3, 3)`` or ``(3, 3)`` torch tensor as a ``wp.array(dtype=wp.mat33f, shape=(1,))``. These Warp kernels read the cell / inv_cell as length-1 ``wp.array(dtype=wp.mat33f)`` and dereference ``cell[0]`` inside the kernel body, matching the cell_list pattern. This avoids a per-call host sync. """ if mat.ndim == 2: mat = mat.unsqueeze(0) return wp.from_torch( mat.detach().contiguous().to(torch.float32), dtype=wp.mat33f, requires_grad=False, return_ctype=True, ) # ============================================================================= # Component ops (torch.library.custom_op wrappers) # ============================================================================= @scoped_torch_warp_stream def _build_cluster_tile_list_impl( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, inv_cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, rebuild_flags: torch.Tensor, use_rebuild_flags: bool, compute_inv_cell: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: """Compute Morton codes + argsort + SoA gather in torch, then run bbox reduction + tile enumeration on the warp side. Triclinic-safe via the (cell, inv_cell) pair; orthorhombic cells just have a diagonal inv_cell and produce the same result as the pre-triclinic implementation. See Also -------- nvalchemiops.neighbors.cluster_tile.build_cluster_tile_list : warp launcher """ device = positions.device if compute_inv_cell: computed_inv_cell, info = torch.linalg.inv_ex(cell, check_errors=False) torch._assert_async(info == 0, "cell matrix must be non-singular") inv_cell.copy_(computed_inv_cell) N = positions.shape[0] if N == 0: return wp_device = str(device) wp_dtype = get_wp_dtype(positions.dtype) if not use_rebuild_flags: num_tiles.zero_() wp_cell = _mat33f_from_torch(cell) wp_inv_cell = _mat33f_from_torch(inv_cell) # ---- Steps 1-3: Morton codes + argsort + gather (torch-side) ---- # Fractional coords via inv_cell; wrap into [0, 1) so the bucket # produces a deterministic 30-bit Morton code. Orthorhombic cells # collapse this back to the cheaper diagonal multiply. # # Pad to ``n_padded = ceil(N / TILE_GROUP_SIZE) * TILE_GROUP_SIZE`` so the # kernels see a 32-aligned layout. Padding slots get a sentinel # max 30-bit Morton code (0x3FFFFFFF) so radix sort places them at # the end of the sorted layout, where the convert/coo kernels' # ``i_sorted < natom`` filter naturally drops any pair involving # them. Padding positions copy the last real atom (any in-cell # position is safe) so the SoA gather has well-defined reads. n_padded = int(sorted_atom_index.shape[0]) if N < n_padded: padded_positions = torch.empty( (n_padded, 3), dtype=positions.dtype, device=positions.device, ) padded_positions[:N].copy_(positions) padded_positions[N:].copy_(positions[-1:].expand(n_padded - N, 3)) else: padded_positions = positions frac = padded_positions @ inv_cell.T 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 codes32 = (_spread(iz) << 2) | (_spread(iy) << 1) | _spread(ix) if N < n_padded: # Sentinel: one bit above any real 30-bit Morton code. Real # codes saturate at 0x3FFFFFFF; 0x40000000 sorts after all of # them. See ``_compute_morton_kernel`` for the matching # warp-side value. codes32[N:] = 0x40000000 morton_codes.copy_(codes32) perm = torch.argsort(codes32) sorted_atom_index.copy_(perm.to(torch.int32)) sorted_pos = padded_positions[perm].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()) # ---- Step 4: rank2group + group2tile (warp launcher) ---- wp_sorted_pos_x = wp.from_torch( sorted_pos_x, dtype=wp_dtype, return_ctype=True, ) wp_sorted_pos_y = wp.from_torch( sorted_pos_y, dtype=wp_dtype, return_ctype=True, ) wp_sorted_pos_z = wp.from_torch( sorted_pos_z, dtype=wp_dtype, return_ctype=True, ) wp_group_ctr_x = wp.from_torch(group_ctr_x, dtype=wp_dtype, return_ctype=True) wp_group_ctr_y = wp.from_torch(group_ctr_y, dtype=wp_dtype, return_ctype=True) wp_group_ctr_z = wp.from_torch(group_ctr_z, dtype=wp_dtype, return_ctype=True) wp_group_ext_x = wp.from_torch(group_ext_x, dtype=wp_dtype, return_ctype=True) wp_group_ext_y = wp.from_torch(group_ext_y, dtype=wp_dtype, return_ctype=True) wp_group_ext_z = wp.from_torch(group_ext_z, dtype=wp_dtype, return_ctype=True) wp_num_tiles = wp.from_torch(num_tiles, dtype=wp.int32, return_ctype=True) wp_tile_row_group = wp.from_torch(tile_row_group, dtype=wp.int32, return_ctype=True) wp_tile_col_group = wp.from_torch( tile_col_group, dtype=wp.int32, return_ctype=True, ) wp_rebuild_flags = wp.from_torch( rebuild_flags, dtype=wp.bool, return_ctype=True, ) wp_build_cluster_tile_list( sorted_pos_x=wp_sorted_pos_x, sorted_pos_y=wp_sorted_pos_y, sorted_pos_z=wp_sorted_pos_z, cell=wp_cell, inv_cell=wp_inv_cell, cutoff=float(cutoff), num_tiles=wp_num_tiles, tile_row_group=wp_tile_row_group, tile_col_group=wp_tile_col_group, wp_dtype=wp_dtype, device=wp_device, group_ctr_x_buffer=wp_group_ctr_x, group_ctr_y_buffer=wp_group_ctr_y, group_ctr_z_buffer=wp_group_ctr_z, group_ext_x_buffer=wp_group_ext_x, group_ext_y_buffer=wp_group_ext_y, group_ext_z_buffer=wp_group_ext_z, rebuild_flags=wp_rebuild_flags if use_rebuild_flags else None, fixed_cell=fixed_cell, qr=( wp.from_torch(qr, dtype=wp.float32, return_ctype=True) if qr is not None else None ), axis_aligned=( wp.from_torch(axis_aligned, dtype=wp.bool, return_ctype=True) if axis_aligned is not None else None ), fractional_rounding_certified=( wp.from_torch( fractional_rounding_certified, dtype=wp.bool, return_ctype=True ) if fractional_rounding_certified is not None else None ), qr_height_certified=( wp.from_torch(qr_height_certified, dtype=wp.bool, return_ctype=True) if qr_height_certified is not None else None ), bbox_cutoff_bounds=( wp.from_torch(bbox_cutoff_bounds, dtype=wp.float32, return_ctype=True) if bbox_cutoff_bounds is not None else None ), ) @torch.library.custom_op( "nvalchemiops::_build_cluster_tile_list", mutates_args=( "sorted_atom_index", "morton_codes", "sorted_pos_x", "sorted_pos_y", "sorted_pos_z", "group_ctr_x", "group_ctr_y", "group_ctr_z", "group_ext_x", "group_ext_y", "group_ext_z", "inv_cell", "num_tiles", "tile_row_group", "tile_col_group", ), ) def _build_cluster_tile_list_op( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, inv_cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, rebuild_flags: torch.Tensor, use_rebuild_flags: bool, compute_inv_cell: bool, ) -> None: _build_cluster_tile_list_impl( positions, cutoff, cell, inv_cell, sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, rebuild_flags, use_rebuild_flags, compute_inv_cell, False, None, None, None, None, None, ) @torch.library.custom_op( "nvalchemiops::_build_cluster_tile_list_fixed", mutates_args=( "sorted_atom_index", "morton_codes", "sorted_pos_x", "sorted_pos_y", "sorted_pos_z", "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", ), ) def _build_cluster_tile_list_fixed_op( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, inv_cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, rebuild_flags: torch.Tensor, use_rebuild_flags: bool, qr: torch.Tensor, axis_aligned: torch.Tensor, fractional_rounding_certified: torch.Tensor, qr_height_certified: torch.Tensor, bbox_cutoff_bounds: torch.Tensor, ) -> None: _build_cluster_tile_list_impl( positions, cutoff, cell, inv_cell, sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, rebuild_flags, use_rebuild_flags, False, True, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ) @_build_cluster_tile_list_op.register_fake def _( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, inv_cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, rebuild_flags: torch.Tensor, use_rebuild_flags: bool, compute_inv_cell: bool, ) -> None: return None @_build_cluster_tile_list_fixed_op.register_fake def _( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, inv_cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, rebuild_flags: torch.Tensor, use_rebuild_flags: bool, qr: torch.Tensor, axis_aligned: torch.Tensor, fractional_rounding_certified: torch.Tensor, qr_height_certified: torch.Tensor, bbox_cutoff_bounds: torch.Tensor, ) -> None: return None def _build_cluster_tile_list_normalized( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, *, inv_cell: torch.Tensor | None = None, rebuild_flags: torch.Tensor | None = None, force_compute_inv_cell: bool = False, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> tuple[torch.Tensor, torch.Tensor]: """Build tiles and return the normalized cell and inverse used by queries.""" if positions.dtype != torch.float32: raise TypeError("positions must be float32") cell_mat = _cell_from_cell(cell).to(positions.dtype) compute_inv_cell = _fixed_cell_geometry is None and ( force_compute_inv_cell or torch.compiler.is_compiling() ) inv_cell_mat = ( torch.empty_like(cell_mat) if inv_cell is None and compute_inv_cell else (torch.linalg.inv(cell_mat).contiguous() if inv_cell is None else inv_cell) ) dummy_rebuild_flags = torch.empty(1, dtype=torch.bool, device=positions.device) build_op = ( _build_cluster_tile_list_fixed_op if _fixed_cell_geometry is not None else _build_cluster_tile_list_op ) build_args = ( positions, cutoff, cell_mat, inv_cell_mat, sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, rebuild_flags if rebuild_flags is not None else dummy_rebuild_flags, rebuild_flags is not None, ) if _fixed_cell_geometry is None: build_op(*build_args, compute_inv_cell) else: geometry = _fixed_cell_geometry if any( value is None for value in ( geometry.qr, geometry.axis_aligned, geometry.fractional_rounding_certified, geometry.qr_height_certified, geometry.bbox_cutoff_bounds, ) ): raise RuntimeError("prepared cluster geometry cache is incomplete") build_op( *build_args, geometry.qr, geometry.axis_aligned, geometry.fractional_rounding_certified, geometry.qr_height_certified, geometry.bbox_cutoff_bounds, ) return cell_mat, inv_cell_mat
[docs] def build_cluster_tile_list( positions: torch.Tensor, cutoff: float, cell: torch.Tensor, sorted_atom_index: torch.Tensor, morton_codes: torch.Tensor, sorted_pos_x: torch.Tensor, sorted_pos_y: torch.Tensor, sorted_pos_z: 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, *, rebuild_flags: torch.Tensor | None = None, ) -> None: """Build cluster-tile neighbor list state into pre-allocated tensors. Normalizes ``cell`` to a ``(3, 3)`` matrix and computes ``inv_cell``, then runs Morton sort (torch) + Warp bounding-box reduction + Warp tile-pair enumeration. Triclinic cells are supported. All output tensors are filled in place. Parameters ---------- positions : torch.Tensor, shape (N, 3), dtype=float32 Atomic coordinates wrapped to the primary cell. Non-32-aligned ``N`` is padded internally to ``ceil(N / TILE_GROUP_SIZE) * TILE_GROUP_SIZE``. cutoff : float Cutoff distance in Cartesian units used for tile-pair pruning. cell : torch.Tensor, shape (1, 3, 3) or (3, 3), dtype=float32 Any non-degenerate cell (orthorhombic or triclinic). sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Output permutation mapping sorted rank to original atom index. Modified in-place. morton_codes : torch.Tensor, shape (n_padded,), dtype=int32 Output scratch buffer for 30-bit Morton codes. Modified in-place. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 Output x-coordinates in Morton-sorted order. Modified in-place. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 Output y-coordinates in Morton-sorted order. Modified in-place. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 Output z-coordinates in Morton-sorted order. Modified in-place. group_ctr_x : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output x-component of group bounding-box centres. Modified in-place. group_ctr_y : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output y-component of group bounding-box centres. Modified in-place. group_ctr_z : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output z-component of group bounding-box centres. Modified in-place. group_ext_x : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output x half-extents of group bounding boxes. Modified in-place. group_ext_y : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output y half-extents of group bounding boxes. Modified in-place. group_ext_z : torch.Tensor, shape (ngroup_padded,), dtype=float32 Output z half-extents of group bounding boxes. Modified in-place. num_tiles : torch.Tensor, shape (1,), dtype=int32 Output atomic counter holding the number of emitted tile pairs. Reset to zero internally before use. Modified in-place. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Output row group index for each emitted tile pair. Modified in-place. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Output column group index for each emitted tile pair. Modified in-place. See Also -------- :func:`nvalchemiops.torch.neighbors.cluster_tile.allocate_cluster_tile_list` : Allocates all buffers consumed by this function. :func:`nvalchemiops.neighbors.cluster_tile.build_cluster_tile_list` : Warp-level launcher called internally. """ _build_cluster_tile_list_normalized( positions, cutoff, cell, sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, rebuild_flags=rebuild_flags, )
@torch.library.custom_op( "nvalchemiops::_query_cluster_tile", mutates_args=( "neighbor_matrix", "neighbor_matrix_shifts", "num_neighbors", "neighbor_vectors", "neighbor_distances", ), ) @scoped_torch_warp_stream def _query_cluster_tile_op( cutoff: float, natom: int, cell: torch.Tensor, inv_cell: torch.Tensor, 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, neighbor_matrix: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, num_neighbors: torch.Tensor, neighbor_vectors: torch.Tensor, neighbor_distances: torch.Tensor, n_tiles: int, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: # ``n_tiles`` (host-synced emitted-tile count from the caller) sets the # launch dimension so we don't launch over the full allocated tile # buffer. The kernel still guards ``tile >= num_tiles[0]`` defensively. # Warp writes active pairs only. Clear padding inside the same opaque # mutation boundary before launching the query. if return_vectors: neighbor_vectors.zero_() if return_distances: neighbor_distances.zero_() device = sorted_pos_x.device wp_device = str(device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_cell = _mat33f_from_torch(cell) wp_inv_cell = _mat33f_from_torch(inv_cell) wp_query_cluster_tile( 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), 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, ), cell=wp_cell, inv_cell=wp_inv_cell, 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, n_tiles=int(n_tiles), return_vectors=bool(return_vectors), return_distances=bool(return_distances), neighbor_vectors=( wp.from_torch(neighbor_vectors, dtype=wp.vec3f, return_ctype=True) if return_vectors else None ), neighbor_distances=( wp.from_torch(neighbor_distances, dtype=wp_dtype, return_ctype=True) if return_distances else None ), **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_query_cluster_tile_op.register_fake def _( cutoff: float, natom: int, cell: torch.Tensor, inv_cell: torch.Tensor, 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, neighbor_matrix: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, num_neighbors: torch.Tensor, neighbor_vectors: torch.Tensor, neighbor_distances: torch.Tensor, n_tiles: int, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None def _query_cluster_tile_normalized( 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, cell: torch.Tensor, cutoff: float, natom: int, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, *, inv_cell_mat: 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, 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, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Convert the tile pair list to neighbor_matrix form in place. Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. Use :func:`nvalchemiops.torch.neighbors.cell_list.cell_list` or :func:`nvalchemiops.torch.neighbors.naive.naive_neighbor_list` for partial neighbor lists. Parameters ---------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation mapping sorted rank to original atom index; output of :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list`. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 x-coordinates in Morton-sorted order. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 y-coordinates in Morton-sorted order. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 z-coordinates in Morton-sorted order. num_tiles : torch.Tensor, shape (1,), dtype=int32 Device-side tile counter written by :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list`. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group indices of emitted tile pairs. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group indices of emitted tile pairs. cell : torch.Tensor, shape (1, 3, 3) or (3, 3), dtype=float32 Simulation cell matrix (orthorhombic or triclinic). cutoff : float Neighbor search cutoff radius in Cartesian units. natom : int True atom count (before padding). 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 per-pair periodic image shift vectors. Modified in-place. cutoff2 : float, optional Second cutoff for a dual-cutoff query; fills ``neighbor_matrix2`` / ``num_neighbors2`` / ``neighbor_matrix_shifts2`` when provided. neighbor_matrix2 : torch.Tensor, shape (natom, max_neighbors), dtype=int32, optional Second-cutoff output neighbor indices. Modified in-place. num_neighbors2 : torch.Tensor, shape (natom,), dtype=int32, optional Second-cutoff per-atom neighbor counts. Modified in-place. neighbor_matrix_shifts2 : torch.Tensor, shape (natom, max_neighbors, 3), dtype=int32, optional Second-cutoff per-pair shift vectors. Modified in-place. rebuild_flags : torch.Tensor, shape (1,), dtype=bool, optional When ``False``, all output buffers are left unchanged and the call returns early. return_vectors : bool, optional Write per-pair Cartesian displacement vectors to ``neighbor_vectors``. Default is ``False``. return_distances : bool, optional Write per-pair scalar distances to ``neighbor_distances``. Default is ``False``. 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, shape (natom, num_parameters), optional Per-atom pair-function parameters; required with ``pair_fn``. neighbor_vectors : torch.Tensor, shape (natom, max_neighbors, 3), optional Output buffer for per-pair displacement vectors. Modified in-place. neighbor_distances : torch.Tensor, shape (natom, max_neighbors), optional Output buffer for per-pair scalar distances. Modified in-place. pair_energies : torch.Tensor, shape (natom, max_neighbors), optional Output buffer for per-pair energies; required with ``pair_fn``. Modified in-place. pair_forces : torch.Tensor, shape (natom, max_neighbors, 3), optional Output buffer for per-pair forces; required with ``pair_fn``. Modified in-place. See Also -------- :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list` : Produces the tile state consumed by this function. :func:`nvalchemiops.torch.neighbors.cluster_tile.query_cluster_tile_coo` : COO-format alternative that emits a flat pair list instead. """ _validate_cluster_tile_matrix_outputs( device=sorted_pos_x.device, dtype=sorted_pos_x.dtype, natom=int(natom), max_neighbors=int(neighbor_matrix.shape[1]), cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, return_vectors=return_vectors, return_distances=return_distances, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, allocate_missing=False, ) cell_mat = _cell_from_cell(cell).to(sorted_pos_x.dtype) if inv_cell_mat is None: inv_cell_mat = torch.linalg.inv(cell_mat).contiguous() else: inv_cell_mat = inv_cell_mat.to(sorted_pos_x.dtype) fixed_geometry_args = _fixed_cluster_op_args(_fixed_cell_geometry) # Eager execution tightens the launch to the emitted tile count. Compiled # execution validates on device and launches the full static capacity. tile_capacity = int(tile_row_group.shape[0]) n_tiles = _check_tile_buffer_capacity(num_tiles, tile_capacity) geometry_only = ( (return_vectors or return_distances) and cutoff2 is None and rebuild_flags is None and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ) feature_path = ( cutoff2 is not None or rebuild_flags 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, ) ) topology_only = not _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 topology_only and (cutoff2 is not None or rebuild_flags is not None): device = sorted_pos_x.device dummy_matrix = torch.empty((1, 1), dtype=torch.int32, device=device) dummy_counts = torch.empty(1, dtype=torch.int32, device=device) dummy_shifts = torch.empty((1, 1, 3), dtype=torch.int32, device=device) dummy_bool = torch.empty(1, dtype=torch.bool, device=device) _query_cluster_tile_topology_op( cell_mat, inv_cell_mat, int(natom), float(cutoff), float(cutoff2) if cutoff2 is not None else 0.0, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2 if neighbor_matrix2 is not None else dummy_matrix, num_neighbors2 if num_neighbors2 is not None else dummy_counts, neighbor_matrix_shifts2 if neighbor_matrix_shifts2 is not None else dummy_shifts, rebuild_flags if rebuild_flags is not None else dummy_bool, int(n_tiles), bool(cutoff2 is not None), bool(rebuild_flags is not None), *fixed_geometry_args, ) return if geometry_only: device = sorted_pos_x.device dummy_vectors = torch.empty((1, 3), dtype=sorted_pos_x.dtype, device=device) dummy_distances = torch.empty(1, dtype=sorted_pos_x.dtype, device=device) _query_cluster_tile_op( cutoff, natom, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, neighbor_vectors if neighbor_vectors is not None else dummy_vectors, neighbor_distances if neighbor_distances is not None else dummy_distances, n_tiles, bool(return_vectors), bool(return_distances), *fixed_geometry_args, ) return if feature_path: if ( pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ): return _query_cluster_tile_optional_no_pair_fn_op( cell_mat, inv_cell_mat, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, rebuild_flags, neighbor_vectors, neighbor_distances, n_tiles, cutoff2, return_vectors, return_distances, *fixed_geometry_args, ) if torch.compiler.is_compiling(): raise NotImplementedError( "cluster_tile pair_fn outputs are eager-only because callable Warp " "functions cannot cross a torch.library.custom_op schema boundary.", ) # Pair outputs are exercised - bypass the torch custom op because it # cannot carry a callable ``pair_fn``. _query_cluster_tile_optional( cell_mat, inv_cell_mat, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, 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, n_tiles=n_tiles, _fixed_cell_geometry=_fixed_cell_geometry, ) return _query_cluster_tile_op( cutoff, natom, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, torch.empty((1, 3), dtype=sorted_pos_x.dtype, device=sorted_pos_x.device), torch.empty(1, dtype=sorted_pos_x.dtype, device=sorted_pos_x.device), n_tiles, False, False, *fixed_geometry_args, )
[docs] def query_cluster_tile( 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, cell: torch.Tensor, cutoff: float, natom: int, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, *, 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, 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, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Convert the tile pair list to neighbor-matrix form in place.""" _query_cluster_tile_normalized( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell, cutoff, natom, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, 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, _fixed_cell_geometry=_fixed_cell_geometry, )
query_cluster_tile.__doc__ = _query_cluster_tile_normalized.__doc__ @torch.library.custom_op( "nvalchemiops::_query_cluster_tile_optional_no_pair_fn", mutates_args=( "neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts", "neighbor_matrix2", "num_neighbors2", "neighbor_matrix_shifts2", "neighbor_vectors", "neighbor_distances", ), ) @scoped_torch_warp_stream def _query_cluster_tile_optional_no_pair_fn_op( cell_mat: torch.Tensor, inv_cell_mat: 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, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_matrix2: torch.Tensor | None, num_neighbors2: torch.Tensor | None, neighbor_matrix_shifts2: torch.Tensor | None, rebuild_flags: torch.Tensor | None, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, n_tiles: int, cutoff2: float | None, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: # Selective queries must retain every output entry when the system is not # rebuilt; a rebuilt system starts with zeroed geometry padding. if rebuild_flags is None: if return_vectors and neighbor_vectors is not None: neighbor_vectors.zero_() if return_distances and neighbor_distances is not None: neighbor_distances.zero_() else: rebuild = rebuild_flags.reshape(-1)[0] if return_vectors and neighbor_vectors is not None: neighbor_vectors.copy_( torch.where( rebuild, torch.zeros_like(neighbor_vectors), neighbor_vectors ) ) if return_distances and neighbor_distances is not None: neighbor_distances.copy_( torch.where( rebuild, torch.zeros_like(neighbor_distances), neighbor_distances, ) ) _query_cluster_tile_optional( cell_mat, inv_cell_mat, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, 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, return_vectors=return_vectors, return_distances=return_distances, pair_fn=None, pair_params=None, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=None, pair_forces=None, _fixed_cell_geometry=_fixed_cluster_geometry_record( inv_cell_mat, fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_query_cluster_tile_optional_no_pair_fn_op.register_fake def _( cell_mat: torch.Tensor, inv_cell_mat: 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, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_matrix2: torch.Tensor | None, num_neighbors2: torch.Tensor | None, neighbor_matrix_shifts2: torch.Tensor | None, rebuild_flags: torch.Tensor | None, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, n_tiles: int, cutoff2: float | None, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @torch.library.custom_op( "nvalchemiops::_query_cluster_tile_topology", mutates_args=( "neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts", "neighbor_matrix2", "num_neighbors2", "neighbor_matrix_shifts2", ), ) @scoped_torch_warp_stream def _query_cluster_tile_topology_op( cell_mat: torch.Tensor, inv_cell_mat: torch.Tensor, natom: int, cutoff: float, cutoff2: 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, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_matrix2: torch.Tensor, num_neighbors2: torch.Tensor, neighbor_matrix_shifts2: torch.Tensor, rebuild_flags: torch.Tensor, n_tiles: int, use_cutoff2: bool, use_rebuild_flags: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: _query_cluster_tile_optional( cell_mat, inv_cell_mat, natom, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, n_tiles=n_tiles, cutoff2=cutoff2 if use_cutoff2 else None, neighbor_matrix2=neighbor_matrix2 if use_cutoff2 else None, num_neighbors2=num_neighbors2 if use_cutoff2 else None, neighbor_matrix_shifts2=neighbor_matrix_shifts2 if use_cutoff2 else None, rebuild_flags=rebuild_flags if use_rebuild_flags else None, return_vectors=False, return_distances=False, pair_fn=None, pair_params=None, neighbor_vectors=None, neighbor_distances=None, pair_energies=None, pair_forces=None, _fixed_cell_geometry=_fixed_cluster_geometry_record( inv_cell_mat, fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_query_cluster_tile_topology_op.register_fake def _( cell_mat: torch.Tensor, inv_cell_mat: torch.Tensor, natom: int, cutoff: float, cutoff2: 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, neighbor_matrix: torch.Tensor, num_neighbors: torch.Tensor, neighbor_matrix_shifts: torch.Tensor, neighbor_matrix2: torch.Tensor, num_neighbors2: torch.Tensor, neighbor_matrix_shifts2: torch.Tensor, rebuild_flags: torch.Tensor, n_tiles: int, use_cutoff2: bool, use_rebuild_flags: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @scoped_torch_warp_stream def _query_cluster_tile_optional( cell_mat: torch.Tensor, inv_cell_mat: 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, 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, 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, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Pair-output path: bypass the torch custom op + call warp directly. Mirrors :func:`nvalchemiops.torch.neighbors.cell_list._query_cell_list_optional`. Torch custom ops cannot carry a callable ``pair_fn`` across their schema boundary, so the pair-output path drops down to the warp launcher with ``wp.from_torch``-wrapped tensors directly. No host-side ``num_tiles.item()`` sync: the warp kernel guards per-tile via the device-side ``num_tiles`` array. """ device = sorted_pos_x.device wp_device = str(device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_vec_dtype = wp.vec3f wp_cell = _mat33f_from_torch(cell_mat) wp_inv_cell = _mat33f_from_torch(inv_cell_mat) wp_pair_params = ( wp.from_torch(pair_params, dtype=wp_dtype, requires_grad=False) if pair_params is not None else None ) wp_neighbor_vectors = ( wp.from_torch(neighbor_vectors, dtype=wp_vec_dtype, requires_grad=False) if neighbor_vectors is not None else None ) wp_neighbor_distances = ( wp.from_torch(neighbor_distances, dtype=wp_dtype, requires_grad=False) if neighbor_distances is not None else None ) wp_pair_energies = ( wp.from_torch(pair_energies, dtype=wp_dtype, requires_grad=False) if pair_energies is not None else None ) wp_pair_forces = ( wp.from_torch(pair_forces, dtype=wp_vec_dtype, requires_grad=False) if pair_forces is not None else None ) wp_query_cluster_tile( sorted_atom_index=wp.from_torch( sorted_atom_index, dtype=wp.int32, requires_grad=False ), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, requires_grad=False), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, requires_grad=False), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, requires_grad=False), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, requires_grad=False), tile_row_group=wp.from_torch( tile_row_group, dtype=wp.int32, requires_grad=False ), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, requires_grad=False ), cell=wp_cell, inv_cell=wp_inv_cell, cutoff=float(cutoff), natom=int(natom), neighbor_matrix=wp.from_torch( neighbor_matrix, dtype=wp.int32, requires_grad=False ), num_neighbors=wp.from_torch(num_neighbors, dtype=wp.int32, requires_grad=False), neighbor_matrix_shifts=wp.from_torch( neighbor_matrix_shifts, dtype=wp.int32, requires_grad=False ), wp_dtype=wp_dtype, device=wp_device, n_tiles=int(n_tiles), cutoff2=cutoff2, neighbor_matrix2=( wp.from_torch(neighbor_matrix2, dtype=wp.int32, requires_grad=False) if neighbor_matrix2 is not None else None ), num_neighbors2=( wp.from_torch(num_neighbors2, dtype=wp.int32, requires_grad=False) if num_neighbors2 is not None else None ), neighbor_matrix_shifts2=( wp.from_torch(neighbor_matrix_shifts2, dtype=wp.int32, requires_grad=False) if neighbor_matrix_shifts2 is not None else None ), rebuild_flags=( wp.from_torch(rebuild_flags, dtype=wp.bool, requires_grad=False) if rebuild_flags 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, **_wp_fixed_cluster_geometry(*_fixed_cluster_op_args(_fixed_cell_geometry)), ) @torch.library.custom_op( "nvalchemiops::_query_cluster_tile_coo", mutates_args=("pair_counter", "coo_list", "coo_shifts"), ) @scoped_torch_warp_stream def _query_cluster_tile_coo_op( cutoff: float, natom: int, max_pairs: int, cell: torch.Tensor, inv_cell: torch.Tensor, 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, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: # ``n_tiles`` (host-synced emitted-tile count from the caller) tightens # 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_cell = _mat33f_from_torch(cell) wp_inv_cell = _mat33f_from_torch(inv_cell) wp_query_cluster_tile_coo( 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), 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, ), cell=wp_cell, inv_cell=wp_inv_cell, 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, n_tiles=int(n_tiles), **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_query_cluster_tile_coo_op.register_fake def _( cutoff: float, natom: int, max_pairs: int, cell: torch.Tensor, inv_cell: torch.Tensor, 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, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @torch.library.custom_op( "nvalchemiops::_query_cluster_tile_coo_segmented", mutates_args=("pair_counter", "pair_counts", "coo_list", "coo_shifts"), ) @scoped_torch_warp_stream def _query_cluster_tile_coo_segmented_op( cutoff: float, natom: int, max_pairs: int, cell: torch.Tensor, inv_cell: torch.Tensor, 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, rebuild_flags: torch.Tensor, pair_counter: torch.Tensor, pair_offsets: torch.Tensor, pair_counts: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: """Run fixed-capacity selective COO conversion in place.""" device = sorted_pos_x.device wp_device = str(device) wp_dtype = get_wp_dtype(sorted_pos_x.dtype) wp_query_cluster_tile_coo( 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), 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), cell=_mat33f_from_torch(cell), inv_cell=_mat33f_from_torch(inv_cell), 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, n_tiles=int(n_tiles), rebuild_flags=wp.from_torch(rebuild_flags, dtype=wp.bool, return_ctype=True), pair_offsets=wp.from_torch(pair_offsets, dtype=wp.int32, return_ctype=True), pair_counts=wp.from_torch(pair_counts, dtype=wp.int32, return_ctype=True), **_wp_fixed_cluster_geometry( fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_query_cluster_tile_coo_segmented_op.register_fake def _( cutoff: float, natom: int, max_pairs: int, cell: torch.Tensor, inv_cell: torch.Tensor, 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, rebuild_flags: torch.Tensor, pair_counter: torch.Tensor, pair_offsets: torch.Tensor, pair_counts: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, n_tiles: int, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @torch.library.custom_op( "nvalchemiops::_query_cluster_tile_coo_optional_no_pair_fn", mutates_args=( "pair_counter", "coo_list", "coo_shifts", "neighbor_vectors", "neighbor_distances", ), ) @scoped_torch_warp_stream def _query_cluster_tile_coo_optional_no_pair_fn_op( cell_mat: torch.Tensor, inv_cell_mat: 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, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, n_tiles: int, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: _query_cluster_tile_coo_optional( cell_mat, inv_cell_mat, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, pair_counter, coo_list, coo_shifts, n_tiles=n_tiles, return_vectors=return_vectors, return_distances=return_distances, pair_fn=None, pair_params=None, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, pair_energies=None, pair_forces=None, _fixed_cell_geometry=_fixed_cluster_geometry_record( inv_cell_mat, fixed_cell, qr, axis_aligned, fractional_rounding_certified, qr_height_certified, bbox_cutoff_bounds, ), ) @_query_cluster_tile_coo_optional_no_pair_fn_op.register_fake def _( cell_mat: torch.Tensor, inv_cell_mat: 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, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, neighbor_vectors: torch.Tensor | None, neighbor_distances: torch.Tensor | None, n_tiles: int, return_vectors: bool, return_distances: bool, fixed_cell: bool, qr: torch.Tensor | None, axis_aligned: torch.Tensor | None, fractional_rounding_certified: torch.Tensor | None, qr_height_certified: torch.Tensor | None, bbox_cutoff_bounds: torch.Tensor | None, ) -> None: return None @scoped_torch_warp_stream def _query_cluster_tile_coo_optional( cell_mat: torch.Tensor, inv_cell_mat: 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, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, *, n_tiles: int, 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, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Pair-output COO path: bypass the torch custom op. ``n_tiles`` (host-synced emitted-tile count) 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, requires_grad=False) if pair_params is not None else None ) wp_neighbor_vectors = ( wp.from_torch(neighbor_vectors, dtype=wp_vec_dtype, requires_grad=False) if neighbor_vectors is not None else None ) wp_neighbor_distances = ( wp.from_torch(neighbor_distances, dtype=wp_dtype, requires_grad=False) if neighbor_distances is not None else None ) wp_pair_energies = ( wp.from_torch(pair_energies, dtype=wp_dtype, requires_grad=False) if pair_energies is not None else None ) wp_pair_forces = ( wp.from_torch(pair_forces, dtype=wp_vec_dtype, requires_grad=False) if pair_forces is not None else None ) wp_query_cluster_tile_coo( sorted_atom_index=wp.from_torch( sorted_atom_index, dtype=wp.int32, requires_grad=False ), sorted_pos_x=wp.from_torch(sorted_pos_x, dtype=wp_dtype, requires_grad=False), sorted_pos_y=wp.from_torch(sorted_pos_y, dtype=wp_dtype, requires_grad=False), sorted_pos_z=wp.from_torch(sorted_pos_z, dtype=wp_dtype, requires_grad=False), num_tiles=wp.from_torch(num_tiles, dtype=wp.int32, requires_grad=False), tile_row_group=wp.from_torch( tile_row_group, dtype=wp.int32, requires_grad=False ), tile_col_group=wp.from_torch( tile_col_group, dtype=wp.int32, requires_grad=False ), cell=_mat33f_from_torch(cell_mat), inv_cell=_mat33f_from_torch(inv_cell_mat), cutoff=float(cutoff), natom=int(natom), max_pairs=int(max_pairs), pair_counter=wp.from_torch(pair_counter, dtype=wp.int32, requires_grad=False), coo_list=wp.from_torch(coo_list, dtype=wp.int32, requires_grad=False), coo_shifts=wp.from_torch(coo_shifts, dtype=wp.int32, requires_grad=False), wp_dtype=wp_dtype, device=wp_device, n_tiles=int(n_tiles), 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, **_wp_fixed_cluster_geometry(*_fixed_cluster_op_args(_fixed_cell_geometry)), )
[docs] def query_cluster_tile_coo( 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, cell: torch.Tensor, cutoff: float, natom: int, max_pairs: int, pair_counter: torch.Tensor, coo_list: torch.Tensor, coo_shifts: torch.Tensor, *, rebuild_flags: 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, _fixed_cell_geometry: _FixedCellGeometry | None = None, ) -> None: """Convert the tile pair list to flat COO format in place. Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. Optional pair outputs use flat COO buffers of length ``max_pairs`` and are written in the same order as ``coo_list``. Selective segmented COO is topology-only and cannot combine with pair outputs. Parameters ---------- sorted_atom_index : torch.Tensor, shape (n_padded,), dtype=int32 Permutation mapping sorted rank to original atom index; output of :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list`. sorted_pos_x : torch.Tensor, shape (n_padded,), dtype=float32 x-coordinates in Morton-sorted order. sorted_pos_y : torch.Tensor, shape (n_padded,), dtype=float32 y-coordinates in Morton-sorted order. sorted_pos_z : torch.Tensor, shape (n_padded,), dtype=float32 z-coordinates in Morton-sorted order. num_tiles : torch.Tensor, shape (1,), dtype=int32 Device-side tile counter written by :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list`. tile_row_group : torch.Tensor, shape (max_tiles,), dtype=int32 Row group indices of emitted tile pairs. tile_col_group : torch.Tensor, shape (max_tiles,), dtype=int32 Column group indices of emitted tile pairs. cell : torch.Tensor, shape (1, 3, 3) or (3, 3), dtype=float32 Simulation cell matrix (orthorhombic or triclinic). cutoff : float Neighbor search cutoff radius in Cartesian units. natom : int True atom count (before padding). max_pairs : int Allocated capacity of the COO output buffers. pair_counter : torch.Tensor, shape (1,), dtype=int32 Atomic counter that accumulates the number of written pairs. Reset to zero internally before use. Modified in-place. coo_list : torch.Tensor, shape (max_pairs, 2), dtype=int32 Output flat pair list; each row is ``(i, j)``. Modified in-place. coo_shifts : torch.Tensor, shape (max_pairs, 3), dtype=int32 Output periodic image shift vectors for each pair. Modified in-place. rebuild_flags : torch.Tensor, shape (1,), dtype=bool, optional Enables selective fixed-capacity COO. Requires ``pair_offsets`` and ``pair_counts``. A false flag preserves the supplied topology buffers; a true flag rebuilds their active prefix. pair_offsets : torch.Tensor, shape (2,), dtype=int32, optional Fixed COO segment boundaries ``[0, max_pairs]``. Must be supplied with ``rebuild_flags`` and ``pair_counts``. pair_counts : torch.Tensor, shape (1,), dtype=int32, optional Number of active pairs in the segment. Modified in-place. return_vectors : bool, optional Write per-pair Cartesian displacement vectors to ``neighbor_vectors``. Default is ``False``. return_distances : bool, optional Write per-pair scalar distances to ``neighbor_distances``. Default is ``False``. 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, shape (natom, num_parameters), optional Per-atom pair-function parameters; required with ``pair_fn``. neighbor_vectors : torch.Tensor, shape (max_pairs, 3), optional Output buffer for per-pair displacement vectors. Modified in-place. neighbor_distances : torch.Tensor, shape (max_pairs,), optional Output buffer for per-pair scalar distances. Modified in-place. pair_energies : torch.Tensor, shape (max_pairs,), optional Output buffer for per-pair energies; required with ``pair_fn``. Modified in-place. pair_forces : torch.Tensor, shape (max_pairs, 3), optional Output buffer for per-pair forces; required with ``pair_fn``. Modified in-place. See Also -------- :func:`nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list` : Produces the tile state consumed by this function. :func:`nvalchemiops.torch.neighbors.cluster_tile.query_cluster_tile` : Row-padded matrix-format alternative. """ segmented_inputs = (rebuild_flags, pair_offsets, pair_counts) segmented = rebuild_flags is not None if any(value is not None for value in segmented_inputs) and not all( value is not None for value in segmented_inputs ): raise ValueError( "rebuild_flags, pair_offsets, and pair_counts must be supplied together" ) if segmented: if _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, ): raise ValueError( "cluster_tile selective rebuild cannot be combined with pair outputs" ) if coo_list.shape != (max_pairs, 2): raise ValueError("coo_list must have shape (max_pairs, 2)") physical_capacity = _validate_segmented_coo_state( device=coo_list.device, num_systems=1, neighbor_list=coo_list.transpose(0, 1), neighbor_list_shifts=coo_shifts, pair_offsets=pair_offsets, pair_counts=pair_counts, rebuild_flags=rebuild_flags, ) if physical_capacity != max_pairs: raise ValueError("max_pairs must equal coo_list.shape[0]") cell_mat, inv_cell_mat = _cell_invcell_from_cell(cell) cell_mat = cell_mat.to(sorted_pos_x.dtype) inv_cell_mat = inv_cell_mat.to(sorted_pos_x.dtype) fixed_geometry_args = _fixed_cluster_op_args(_fixed_cell_geometry) # Eager execution tightens the launch to the emitted tile count. Compiled # execution validates on device and launches the full static capacity. tile_capacity = int(tile_row_group.shape[0]) n_tiles = _check_tile_buffer_capacity(num_tiles, tile_capacity) pair_counter.zero_() if segmented: _query_cluster_tile_coo_segmented_op( cutoff, natom, max_pairs, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, rebuild_flags, pair_counter, pair_offsets, pair_counts, coo_list, coo_shifts, n_tiles, *fixed_geometry_args, ) if torch.compiler.is_compiling(): _normalize_compiled_single_segment_coo_count( pair_offsets=pair_offsets, pair_counts=pair_counts, rebuild_flags=rebuild_flags, physical_capacity=physical_capacity, ) return if _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 ( pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ): return _query_cluster_tile_coo_optional_no_pair_fn_op( cell_mat, inv_cell_mat, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, pair_counter, coo_list, coo_shifts, neighbor_vectors, neighbor_distances, n_tiles, return_vectors, return_distances, *fixed_geometry_args, ) if torch.compiler.is_compiling(): raise NotImplementedError( "cluster_tile COO pair_fn outputs are eager-only because callable " "Warp functions cannot cross a torch.library.custom_op schema boundary.", ) _query_cluster_tile_coo_optional( cell_mat, inv_cell_mat, natom, max_pairs, cutoff, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, pair_counter, coo_list, coo_shifts, n_tiles=n_tiles, 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, _fixed_cell_geometry=_fixed_cell_geometry, ) return _query_cluster_tile_coo_op( cutoff, natom, max_pairs, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, pair_counter, coo_list, coo_shifts, n_tiles, *fixed_geometry_args, )
@scoped_torch_warp_stream def _cluster_tile_neighbor_list_impl( positions: torch.Tensor, cutoff: float | None = None, cell: torch.Tensor | None = None, 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, return_state: bool = False, # 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_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, pair_counter: torch.Tensor | None = None, # scratch buffers sorted_atom_index: torch.Tensor | None = None, morton_codes: 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, 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, # 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, eager_rebuild_count: int | None = None, inv_cell: torch.Tensor | None = None, force_compute_inv_cell: bool = False, _fixed_cell_geometry: _FixedCellGeometry | None = None, _live_cell: torch.Tensor | None = None, ) -> tuple[torch.Tensor, ...]: """Build and query a cluster-pair tile neighbor list in one call. Single-system PyTorch binding for the cluster-pair tile algorithm. Runs Morton sort, Warp bounding-box reduction, and tile enumeration, then emits the result in one of three formats selected by ``format=``. Supports orthorhombic and triclinic cells alike via ``_wrap_triclinic``. Cluster-tile is CUDA float32 only. Parameters ---------- positions : torch.Tensor, shape (N, 3), dtype=float32 Atomic coordinates. Any ``N >= 0``; non-32-aligned ``N`` is supported via internal padding to ``ceil(N / TILE_GROUP_SIZE) * TILE_GROUP_SIZE``. Padding slots use sentinel Morton codes and are filtered out by the convert/coo kernels. cutoff : float, optional Cutoff distance in Cartesian units. Must be positive. cutoff2 : float, optional Cutoff for the second matrix. It is normally the outer cutoff and may equal ``cutoff``. Either order is accepted because the tile buffer is sized for the larger value. Cannot be combined with pair outputs or COO/tile formats. cell : torch.Tensor, shape (1, 3, 3) or (3, 3), dtype=float32 Any non-degenerate cell (orthorhombic or triclinic). Required on every call. max_neighbors : int, optional Falls back to ``estimate_max_neighbors`` using the larger active cutoff. Matrix format only. fill_value : int, optional Matrix sentinel; defaults to ``N``. format : {"matrix", "coo", "tile"}, default "matrix" Output representation: - ``"matrix"``: returns ``(neighbor_matrix, num_neighbors, neighbor_matrix_shifts)`` — the dense ``(N, max_neighbors)`` row-padded form used by ``cell_list`` and ``naive``. - ``"coo"``: compact calls return ``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` and append exact distances, vectors, or both when requested. The geometry order is distances followed by vectors. The trimmed flat pair list is placed directly into source-owned CSR rows; pair order within a row is unspecified. With ``rebuild_flags``, fixed-capacity segmented COO returns ``(neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts)`` instead. - ``"tile"``: returns the native cluster-pair tile state as a 7-tuple ``(num_tiles, tile_row_group, tile_col_group, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z)``. No convert kernel is run. Intended for downstream kernels that consume the tile-pair list directly with shared-memory tile loads. Tile pairs are group-level half-fill: every emitted pair has ``tile_col_group[t] >= tile_row_group[t]``. The consumer chooses atom-level fill. max_pairs : int, optional Upper bound for COO output; defaults to ``N * max_neighbors``. max_tiles_per_group : int, optional Capacity factor for an internally allocated intermediate tile-pair buffer. For ``g`` row groups, the buffer holds ``g * min(g, max_tiles_per_group)`` tile pairs. Increasing the value up to ``g`` uses more memory and accommodates more candidate tile pairs. Eager calls estimate the value when it is ``None``. Caller-owned tile arrays determine the actual capacity. See :ref:`cluster-tile-buffer-capacity` for sizing details. rebuild_flags : torch.Tensor, shape (1,), dtype=bool, optional Selective rebuild flag. An eager all-true call may bootstrap omitted state. When false, caller-owned fixed topology and tile state are required before launch. COO calls additionally require ``pair_offsets`` and ``pair_counts``. The explicit single-system COO route supports ``torch.compile(fullgraph=True)`` only after eager bootstrap with fixed-shape validated buffers. return_state : bool, default=False With ``rebuild_flags``, append ``(num_tiles, tile_row_group, tile_col_group)`` after topology outputs. These are the same tensor objects as caller-supplied state buffers. neighbor_matrix, num_neighbors, neighbor_matrix_shifts : optional Pre-allocated matrix-format outputs. All-or-nothing only across the trio; supply all three or none. neighbor_list, neighbor_list_shifts, pair_counter : optional Pre-allocated compact COO outputs with shapes ``(2, max_pairs)``, ``(max_pairs, 3)``, and ``(1,)``, dtype int32 and on the same device as ``positions``. Each buffer may be supplied independently; omitted buffers are allocated. pair_offsets, pair_counts : optional Fixed selective-COO segment metadata with shapes ``(2,)`` and ``(1,)`` int32. Requires ``rebuild_flags``, caller-owned ``neighbor_list``, and ``neighbor_list_shifts``. sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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 : torch.Tensor, optional Pre-allocated scratch buffers (shapes as returned by ``allocate_cluster_tile_list``). All-or-nothing: either provide every scratch buffer or none. The trigger is ``sorted_atom_index``. Reuse is safe — ``num_tiles`` is reset each call and every other scratch tensor is either fully overwritten or only read in regions the kernels just wrote. return_vectors, return_distances : bool, default ``False`` Return per-pair Cartesian displacements / scalar distances and write their active prefixes to ``neighbor_vectors`` / ``neighbor_distances`` when those buffers are supplied. Matrix format uses ``(N, max_neighbors, ...)`` buffers; COO format uses flat ``(max_pairs, ...)`` buffers. pair_fn : callable, optional Module-scope Warp ``@wp.func`` of signature ``(r_ij, distance, pair_params, i, j) -> (energy, force)``. Writes per-pair energies / forces to ``pair_energies`` / ``pair_forces``. Matrix format uses row-padded buffers; COO format uses flat buffers written in pair-list order. pair_params : torch.Tensor, shape ``(num_atoms, num_parameters)``, optional Per-atom pair-function parameters; required with ``pair_fn``. neighbor_vectors, neighbor_distances : torch.Tensor, optional OUTPUT buffers for per-pair displacements / distances. Without autograd reconstruction, matrix and compact COO formats allocate them when omitted. Compact COO accepts flat capacity buffers, leaves their inactive tails unspecified, and returns newly allocated exact geometry that does not alias them. Geometry buffers must not require gradients. When geometry is reconstructed for autograd, supplied buffers receive detached value snapshots while the returned geometry uses separate differentiable tensors. Omitted geometry buffers are not allocated. pair_energies, pair_forces : torch.Tensor, optional OUTPUT buffers for per-pair energies / forces. Matrix format allocates them when omitted; COO format requires caller-owned flat buffers. 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. With ``cutoff2``, returns the primary group followed by the secondary cutoff group. Selective calls with ``return_state=True`` append ``(num_tiles, tile_row_group, tile_col_group)``, yielding six tensors for one cutoff or nine for two cutoffs. Differentiable reconstructed geometry does not alias supplied output buffers; otherwise returned geometry is the supplied or internally allocated buffer. - ``"coo"``: compact calls return ``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` and append ``distances`` and/or ``vectors`` in that order when requested without a pair callback. Exact outputs do not alias reusable capacity buffers. Pair callbacks keep the topology-only return and write their buffers in place. Selective calls return fixed-capacity ``(neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts)`` and append tile state when ``return_state=True``. - ``"tile"``: ``(num_tiles, tile_row_group, tile_col_group, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z)``. Notes ----- - Cluster-tile is CUDA float32 only; float64 ``positions`` is rejected. - Cluster-tile does not support partial neighbor lists (no ``target_indices`` kwarg). - ``torch.compile(fullgraph=True)`` supports tile and matrix output, including dual-cutoff matrices and differentiable geometry. On PyTorch 2.10 or newer, nonselective exact COO output also supports fullgraph and may return a different pair count on each call. A compiled call that allocates scratch internally requires a positive static ``max_tiles_per_group``; complete caller-owned scratch may be supplied instead. Compiled capacity failures use asynchronous device assertions. Pair callbacks remain eager-only. - Build differentiable losses from returned geometry, not from supplied output buffers, which are non-differentiable value snapshots when reconstruction is required. - 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="cluster_tile"`` to force it. See Also -------- nvalchemiops.torch.neighbors.batch_cluster_tile_neighbor_list : Batched companion entry point. nvalchemiops.torch.neighbors.cluster_tile.build_cluster_tile_list : Lower-level build step exposed for caching across queries. nvalchemiops.torch.neighbors.cluster_tile.query_cluster_tile : Lower-level query step. """ if cutoff is None or cell is None: missing = [ name for name, value in (("cutoff", cutoff), ("cell", cell)) if value is None ] raise ValueError("missing required arguments: " + ", ".join(missing)) 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}", ) if max_tiles_per_group is not None and ( not isinstance(max_tiles_per_group, int) or isinstance(max_tiles_per_group, bool) or max_tiles_per_group <= 0 ): raise ValueError("max_tiles_per_group must be a positive integer") 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 ) dual_cutoff = cutoff2 is not None selective = rebuild_flags is not None is_compiling = torch.compiler.is_compiling() if is_compiling and format == "coo" and not selective and pair_fn is not None: raise NotImplementedError( "torch.compile(fullgraph=True) cluster-tile pair_fn is not supported" ) if selective and not is_compiling: if eager_rebuild_count is None: eager_rebuild_count = int(torch.count_nonzero(rebuild_flags).item()) eager_rebuild_requested = eager_rebuild_count > 0 else: eager_rebuild_requested = False if return_state and not selective: raise ValueError("return_state=True requires rebuild_flags") if not selective and (pair_offsets is not None or pair_counts is not None): raise ValueError("pair_offsets and pair_counts require rebuild_flags") 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 dual_cutoff: 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 selective: if format not in ("matrix", "coo"): raise ValueError( "cluster_tile selective rebuild is supported only with " "format='matrix' or format='coo'" ) if has_pair_outputs: raise ValueError( "cluster_tile selective rebuild cannot be combined with pair outputs" ) required = { "num_tiles": num_tiles, "tile_row_group": tile_row_group, "tile_col_group": tile_col_group, } if format == "matrix": required.update( { "neighbor_matrix": neighbor_matrix, "num_neighbors": num_neighbors, "neighbor_matrix_shifts": neighbor_matrix_shifts, } ) else: required.update( { "neighbor_list": neighbor_list, "pair_offsets": pair_offsets, "pair_counts": pair_counts, "neighbor_list_shifts": neighbor_list_shifts, } ) if dual_cutoff: required.update( { "neighbor_matrix2": neighbor_matrix2, "num_neighbors2": num_neighbors2, "neighbor_matrix_shifts2": neighbor_matrix_shifts2, } ) missing = [name for name, value in required.items() if value is None] bootstrap_coo = ( format == "coo" and eager_rebuild_requested and bool(return_state) ) if missing and not bootstrap_coo: raise ValueError( "rebuild_flags requires previous cluster_tile state: " + ", ".join(missing) ) N = positions.shape[0] device = positions.device if max_neighbors is None: max_neighbors = ( int(neighbor_matrix.shape[1]) if format == "matrix" and neighbor_matrix is not None else max( estimate_max_neighbors( cutoff if cutoff2 is None else max(float(cutoff), float(cutoff2)) ), TILE_GROUP_SIZE, ) ) elif ( format == "matrix" and neighbor_matrix is not None and (neighbor_matrix.shape != (N, int(max_neighbors))) ): raise ValueError("neighbor_matrix must have shape (N, max_neighbors)") if format == "matrix": _validate_cluster_tile_matrix_outputs( device=device, dtype=positions.dtype, natom=N, max_neighbors=int(max_neighbors), cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, return_vectors=return_vectors, return_distances=return_distances, neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, allocate_missing=True, ) if fill_value is None: fill_value = N if selective and format == "matrix": matrix_state = { "neighbor_matrix": neighbor_matrix, "num_neighbors": num_neighbors, "neighbor_matrix_shifts": neighbor_matrix_shifts, "num_tiles": num_tiles, "tile_row_group": tile_row_group, "tile_col_group": tile_col_group, } for name, value in matrix_state.items(): if value is None or value.device != device or value.dtype != torch.int32: raise ValueError(f"{name} must be an int32 tensor on positions.device") if ( num_neighbors.shape != (N,) or neighbor_matrix_shifts.shape != (N, int(max_neighbors), 3) or num_tiles.shape != (1,) or tile_row_group.ndim != 1 or tile_col_group.shape != tile_row_group.shape ): raise ValueError("selective matrix state has an invalid shape") if dual_cutoff and ( neighbor_matrix2 is None or num_neighbors2 is None or neighbor_matrix_shifts2 is None or neighbor_matrix2.dtype != torch.int32 or num_neighbors2.dtype != torch.int32 or neighbor_matrix_shifts2.dtype != torch.int32 or neighbor_matrix2.device != device or num_neighbors2.device != device or neighbor_matrix_shifts2.device != device or neighbor_matrix2.shape != (N, int(max_neighbors)) or num_neighbors2.shape != (N,) or neighbor_matrix_shifts2.shape != (N, int(max_neighbors), 3) ): raise ValueError("selective secondary matrix state has an invalid shape") if selective and format == "coo" and eager_rebuild_requested: coo_state = ( neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts, num_tiles, tile_row_group, tile_col_group, ) if any(value is not None for value in coo_state) and any( value is None for value in coo_state ): raise ValueError( "selective COO bootstrap requires complete caller-owned state " "or no persistent state" ) if neighbor_list is None: capacity = max_pairs if max_pairs is not None else N * max_neighbors neighbor_list = torch.empty( (2, capacity), dtype=torch.int32, device=positions.device ) neighbor_list_shifts = torch.empty( (capacity, 3), dtype=torch.int32, device=positions.device ) pair_offsets = torch.tensor( [0, capacity], dtype=torch.int32, device=positions.device ) pair_counts = torch.zeros(1, dtype=torch.int32, device=positions.device) if selective and format == "coo": max_pairs_from_buffers = _validate_segmented_coo_state( device=positions.device, num_systems=1, neighbor_list=neighbor_list, neighbor_list_shifts=neighbor_list_shifts, pair_offsets=pair_offsets, pair_counts=pair_counts, rebuild_flags=rebuild_flags, ) if max_pairs is not None and int(max_pairs) != max_pairs_from_buffers: raise ValueError("max_pairs must equal neighbor_list.shape[1]") if selective and not is_compiling and not eager_rebuild_requested: if format == "coo": outputs = ( neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts, ) elif dual_cutoff: outputs = ( neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, ) else: outputs = (neighbor_matrix, num_neighbors, neighbor_matrix_shifts) if return_state: return (*outputs, num_tiles, tile_row_group, tile_col_group) return outputs geometry_requested = ( (return_vectors or return_distances) and pair_fn is None and pair_params is None and pair_energies is None and pair_forces is None ) requires_reconstruction = ( geometry_requested and torch.is_grad_enabled() and ( positions.requires_grad or (_live_cell if _live_cell is not None else cell).requires_grad ) ) snapshot_vectors = neighbor_vectors is not None snapshot_distances = neighbor_distances is not None if format == "coo" and not selective: if max_pairs is None: max_pairs = N * max_neighbors _validate_compact_coo_outputs( device=device, capacity=int(max_pairs), neighbor_list=neighbor_list, neighbor_list_shifts=neighbor_list_shifts, pair_counter=pair_counter, ) if geometry_requested: neighbor_vectors, neighbor_distances = ( _prepare_compact_coo_geometry_buffers( device=device, dtype=positions.dtype, capacity=int(max_pairs), return_vectors=bool(return_vectors), return_distances=bool(return_distances), neighbor_vectors=neighbor_vectors, neighbor_distances=neighbor_distances, allocate_missing=not requires_reconstruction, ) ) # Candidate tiles must cover both radii. The query then filters each matrix # with its own cutoff. build_cutoff = cutoff if cutoff2 is None else max(float(cutoff), float(cutoff2)) # Allocate scratch if caller didn't supply. ``sorted_atom_index`` is the # all-or-nothing sentinel. if sorted_atom_index is None: previous_tile_state = (num_tiles, tile_row_group, tile_col_group) if max_tiles_per_group is not None: max_tiles_per_group = int(max_tiles_per_group) elif is_compiling and not selective: raise ValueError( "compiled cluster_tile_neighbor_list requires max_tiles_per_group " "when scratch buffers are not provided" ) elif selective and torch.compiler.is_compiling(): if tile_row_group is None: raise ValueError( "compiled selective cluster_tile_neighbor_list requires " "tile_row_group" ) groups = max(1, (N + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE) max_tiles_per_group = max(1, int(tile_row_group.shape[0]) // groups) else: cell_volume = float(_cell_volume(cell)) max_tiles_per_group = estimate_max_tiles_per_group( N, build_cutoff, cell_volume, ) ( sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, ) = allocate_cluster_tile_list( N, device, dtype=positions.dtype, max_tiles_per_group=max_tiles_per_group, ) if selective: previous_num_tiles, previous_row, previous_col = previous_tile_state if previous_num_tiles is not None: num_tiles = previous_num_tiles if previous_row is not None: tile_row_group = previous_row if previous_col is not None: tile_col_group = previous_col cell_mat, inv_cell_mat = _build_cluster_tile_list_normalized( positions.detach() if requires_reconstruction else positions, build_cutoff, cell.detach() if requires_reconstruction else cell, sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, 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, inv_cell=inv_cell, rebuild_flags=rebuild_flags if selective else None, force_compute_inv_cell=force_compute_inv_cell, _fixed_cell_geometry=_fixed_cell_geometry, ) if format == "tile": # Raw-tile callers must not receive a silently truncated tile list. tile_capacity = int(tile_row_group.shape[0]) _check_tile_buffer_capacity(num_tiles, tile_capacity) return ( num_tiles, tile_row_group, tile_col_group, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, ) if format == "coo": if selective: max_pairs = int(neighbor_list.shape[1]) elif max_pairs is None: max_pairs = N * max_neighbors # Kernels write row-major (max_pairs, 2); transpose to the public # (2, num_pairs) layout on return. Pre-allocation kwargs accept the # public (2, max_pairs) layout. if neighbor_list is None: coo_buf = torch.empty( (max_pairs, 2), dtype=torch.int32, device=device, ) else: # Caller passed (2, max_pairs). Use a transposed view; the # kernel writes row-major into this buffer. 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 not selective: if is_compiling: version = torch.__version__.split("+", maxsplit=1)[0] major, minor = (int(part) for part in version.split(".")[:2]) if (major, minor) < (2, 10): raise RuntimeError( "torch.compile(fullgraph=True) exact cluster-tile COO requires PyTorch 2.10 or newer" ) copy_vectors = bool( geometry_requested and return_vectors and not requires_reconstruction ) copy_distances = bool( geometry_requested and return_distances and not requires_reconstruction ) if not geometry_requested: prefix_vectors = torch.empty( (1, 3), dtype=positions.dtype, device=device ) prefix_distances = torch.empty(1, dtype=positions.dtype, device=device) if geometry_requested: prefix_vectors = neighbor_vectors prefix_distances = neighbor_distances tile_capacity = int(tile_row_group.shape[0]) n_tiles = _check_tile_buffer_capacity(num_tiles, tile_capacity) launch_tiles = tile_capacity if is_compiling else n_tiles row_counts = torch.zeros(N, dtype=torch.int32, device=device) _cluster_tile_direct_csr_count_op( cutoff, N, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, row_counts, launch_tiles, *_fixed_cluster_op_args(_fixed_cell_geometry), ) neighbor_ptr = torch.cat( ( torch.zeros(1, dtype=torch.int32, device=device), torch.cumsum(row_counts, dim=0, dtype=torch.int32), ) ) total = neighbor_ptr[-1:] resolved_count = _check_neighbor_capacity( total, int(max_pairs), kind="coo", ) pair_counter.copy_(total) cursors = neighbor_ptr[:-1].clone() if pair_fn is None: _cluster_tile_direct_csr_fill_op( cutoff, N, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cursors, coo_buf, neighbor_list_shifts, prefix_vectors, prefix_distances, copy_vectors, copy_distances, int(max_pairs), launch_tiles, *_fixed_cluster_op_args(_fixed_cell_geometry), ) else: if pair_params is None or pair_energies is None or pair_forces is None: raise ValueError( "pair_fn requires pair_params, pair_energies, and pair_forces" ) _cluster_tile_direct_csr_fill_pair_outputs( cutoff, N, cell_mat, inv_cell_mat, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cursors, coo_buf, neighbor_list_shifts, pair_fn, pair_params, pair_energies, pair_forces, neighbor_vectors, neighbor_distances, return_vectors, return_distances, int(max_pairs), launch_tiles, _fixed_cell_geometry=_fixed_cell_geometry, ) nl, nls, exact_vectors, exact_distances = _compact_coo_prefix( total, coo_buf, neighbor_list_shifts, prefix_vectors, prefix_distances, int(max_pairs), -1 if is_compiling else resolved_count, copy_vectors, copy_distances, ) if geometry_requested: if requires_reconstruction: exact_distances, exact_vectors = _reconstruct_coo_geometry( positions, _live_cell if _live_cell is not None else cell, nl, nls, ) if return_vectors and snapshot_vectors: neighbor_vectors[: exact_vectors.shape[0]].copy_( exact_vectors.detach() ) if return_distances and snapshot_distances: neighbor_distances[: exact_distances.shape[0]].copy_( exact_distances.detach() ) outputs = (nl, neighbor_ptr, nls) if return_distances: outputs = (*outputs, exact_distances) if return_vectors: outputs = (*outputs, exact_vectors) return outputs return nl, neighbor_ptr, nls query_cluster_tile_coo( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell, cutoff, N, int(max_pairs), pair_counter, coo_buf, neighbor_list_shifts, rebuild_flags=rebuild_flags, 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, _fixed_cell_geometry=_fixed_cell_geometry, ) if selective: if not torch.compiler.is_compiling(): pair_capacity = int(pair_offsets[1].item()) - int( pair_offsets[0].item() ) _check_neighbor_capacity( pair_counts, pair_capacity, kind="coo", ) outputs = ( neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts, ) if return_state: return (*outputs, num_tiles, tile_row_group, tile_col_group) return outputs # ``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 selective: _cluster_tile_selective_zero_num_neighbors_single_op( num_neighbors, rebuild_flags, ) else: num_neighbors.zero_() if neighbor_matrix_shifts is None: neighbor_matrix_shifts = torch.empty( (N, max_neighbors, 3), dtype=torch.int32, device=device, ) if dual_cutoff: 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 selective: _cluster_tile_selective_zero_num_neighbors_single_op( num_neighbors2, rebuild_flags, ) else: num_neighbors2.zero_() if neighbor_matrix_shifts2 is None: neighbor_matrix_shifts2 = torch.empty( (N, max_neighbors, 3), dtype=torch.int32, device=device ) # Pair-output buffer allocation: caller may omit any of the four OUTPUT # buffer kwargs. Reconstructed geometry needs no internal snapshot buffer; # it is returned directly and copied only to caller-owned storage. # Required-presence rules # (``return_vectors`` ⇒ ``neighbor_vectors``, # ``pair_fn`` ⇒ ``pair_{energies,forces}_buffer``) are enforced by # the warp launcher. if return_vectors and neighbor_vectors is None and not requires_reconstruction: neighbor_vectors = torch.empty( (N, max_neighbors, 3), dtype=positions.dtype, device=device, ) if return_distances and neighbor_distances is None and not requires_reconstruction: 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, ) _query_cluster_tile_normalized( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell, cutoff, N, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, inv_cell_mat=inv_cell_mat, cutoff2=cutoff2, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, rebuild_flags=rebuild_flags, return_vectors=return_vectors and not requires_reconstruction, return_distances=return_distances and not requires_reconstruction, pair_fn=pair_fn, pair_params=pair_params, neighbor_vectors=None if requires_reconstruction else neighbor_vectors, neighbor_distances=None if requires_reconstruction else neighbor_distances, pair_energies=pair_energies, pair_forces=pair_forces, _fixed_cell_geometry=_fixed_cell_geometry, ) # Skip-prefill tail fill: write ``fill_value`` into the unused columns # of ``neighbor_matrix``. Pairs with the always-write-shifts kernel # above to eliminate the per-step ``neighbor_matrix.fill_`` and # ``neighbor_matrix_shifts.zero_`` ops. if max_neighbors > 0: if selective: _cluster_tile_selective_fill_neighbor_matrix_tail_op( num_neighbors, rebuild_flags, neighbor_matrix, int(max_neighbors), int(fill_value), ) if dual_cutoff: _cluster_tile_selective_fill_neighbor_matrix_tail_op( num_neighbors2, rebuild_flags, neighbor_matrix2, int(max_neighbors), int(fill_value), ) else: _cluster_tile_fill_neighbor_matrix_tail_op( num_neighbors, neighbor_matrix, int(N), int(max_neighbors), int(fill_value), ) if dual_cutoff: _cluster_tile_fill_neighbor_matrix_tail_op( num_neighbors2, neighbor_matrix2, int(N), int(max_neighbors), int(fill_value), ) _check_neighbor_capacity(num_neighbors, int(max_neighbors)) if dual_cutoff and num_neighbors2 is not None: _check_neighbor_capacity(num_neighbors2, int(max_neighbors)) if requires_reconstruction: distances, vectors = _reconstruct_matrix_geometry( positions, _live_cell if _live_cell is not None else cell, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, ) if return_vectors and snapshot_vectors: neighbor_vectors.copy_(vectors.detach()) if return_distances and snapshot_distances: neighbor_distances.copy_(distances.detach()) if dual_cutoff: outputs = ( neighbor_matrix, num_neighbors, neighbor_matrix_shifts, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, ) else: outputs = (neighbor_matrix, num_neighbors, neighbor_matrix_shifts) if geometry_requested: if return_distances: outputs = ( *outputs, distances if requires_reconstruction else neighbor_distances, ) if return_vectors: outputs = ( *outputs, vectors if requires_reconstruction else neighbor_vectors, ) if return_state: return (*outputs, num_tiles, tile_row_group, tile_col_group) return outputs
[docs] def cluster_tile_neighbor_list( positions: torch.Tensor, cutoff: float, cell: 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, return_state: bool = False, 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, neighbor_list: torch.Tensor | None = None, neighbor_list_shifts: torch.Tensor | None = None, pair_offsets: torch.Tensor | None = None, pair_counts: torch.Tensor | None = None, pair_counter: torch.Tensor | None = None, sorted_atom_index: torch.Tensor | None = None, morton_codes: 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, 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, 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 and query a cluster-pair tile neighbor list in one call.""" return _cluster_tile_neighbor_list_impl( positions, cutoff, cell, max_neighbors=max_neighbors, fill_value=fill_value, format=format, max_pairs=max_pairs, cutoff2=cutoff2, rebuild_flags=rebuild_flags, return_state=return_state, neighbor_matrix=neighbor_matrix, neighbor_matrix_shifts=neighbor_matrix_shifts, num_neighbors=num_neighbors, neighbor_matrix2=neighbor_matrix2, neighbor_matrix_shifts2=neighbor_matrix_shifts2, num_neighbors2=num_neighbors2, neighbor_list=neighbor_list, neighbor_list_shifts=neighbor_list_shifts, pair_offsets=pair_offsets, pair_counts=pair_counts, pair_counter=pair_counter, sorted_atom_index=sorted_atom_index, morton_codes=morton_codes, sorted_pos_x=sorted_pos_x, sorted_pos_y=sorted_pos_y, sorted_pos_z=sorted_pos_z, group_ctr_x=group_ctr_x, group_ctr_y=group_ctr_y, group_ctr_z=group_ctr_z, group_ext_x=group_ext_x, group_ext_y=group_ext_y, group_ext_z=group_ext_z, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, 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_tiles_per_group=max_tiles_per_group, )
cluster_tile_neighbor_list.__doc__ = _cluster_tile_neighbor_list_impl.__doc__