Source code for nvalchemiops.torch.neighbors.naive_dual_cutoff

# 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 unbatched naive dual cutoff neighbor list construction."""

from __future__ import annotations

import torch
import warp as wp

from nvalchemiops.neighbors.naive import (
    naive_neighbor_matrix_dual_cutoff,
    naive_neighbor_matrix_pbc_dual_cutoff,
)
from nvalchemiops.neighbors.neighbor_utils import (
    estimate_max_neighbors,
    selective_zero_num_neighbors_single,
)
from nvalchemiops.torch._warp_op_helpers import register_noop_fake
from nvalchemiops.torch.neighbors.neighbor_utils import (
    compute_naive_num_shifts,
    get_neighbor_list_from_neighbor_matrix,
)
from nvalchemiops.torch.types import get_wp_dtype, get_wp_mat_dtype, get_wp_vec_dtype

__all__ = ["naive_neighbor_list_dual_cutoff"]


@torch.library.custom_op(
    "nvalchemiops::_naive_neighbor_matrix_no_pbc_dual_cutoff",
    mutates_args=(
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_matrix2",
        "num_neighbors2",
    ),
)
def _naive_neighbor_matrix_no_pbc_dual_cutoff(
    positions: torch.Tensor,
    cutoff1: float,
    cutoff2: float,
    neighbor_matrix1: torch.Tensor,
    num_neighbors1: torch.Tensor,
    neighbor_matrix2: torch.Tensor,
    num_neighbors2: torch.Tensor,
    half_fill: bool = False,
) -> None:
    """Fill two neighbor matrices for atoms using dual cutoffs with naive O(N^2) algorithm.

    This function is torch compilable.

    See Also
    --------
    nvalchemiops.neighbors.naive_dual_cutoff.naive_neighbor_matrix_dual_cutoff : Core warp launcher
    naive_neighbor_list_dual_cutoff : High-level wrapper function
    """
    device = positions.device
    wp_vec_dtype = get_wp_vec_dtype(positions.dtype)
    wp_dtype = get_wp_dtype(positions.dtype)

    wp_positions = wp.from_torch(
        positions, dtype=wp_vec_dtype, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix1 = wp.from_torch(
        neighbor_matrix1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors1 = wp.from_torch(
        num_neighbors1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix2 = wp.from_torch(
        neighbor_matrix2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors2 = wp.from_torch(
        num_neighbors2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )

    naive_neighbor_matrix_dual_cutoff(
        positions=wp_positions,
        cutoff1=cutoff1,
        cutoff2=cutoff2,
        neighbor_matrix1=wp_neighbor_matrix1,
        num_neighbors1=wp_num_neighbors1,
        neighbor_matrix2=wp_neighbor_matrix2,
        num_neighbors2=wp_num_neighbors2,
        wp_dtype=wp_dtype,
        device=str(device),
        half_fill=half_fill,
    )


@torch.library.custom_op(
    "nvalchemiops::_naive_neighbor_matrix_pbc_dual_cutoff",
    mutates_args=(
        "neighbor_matrix1",
        "neighbor_matrix2",
        "neighbor_matrix_shifts1",
        "neighbor_matrix_shifts2",
        "num_neighbors1",
        "num_neighbors2",
    ),
)
def _naive_neighbor_matrix_pbc_dual_cutoff(
    positions: torch.Tensor,
    cutoff1: float,
    cutoff2: float,
    cell: torch.Tensor,
    pbc: torch.Tensor,
    neighbor_matrix1: torch.Tensor,
    neighbor_matrix2: torch.Tensor,
    neighbor_matrix_shifts1: torch.Tensor,
    neighbor_matrix_shifts2: torch.Tensor,
    num_neighbors1: torch.Tensor,
    num_neighbors2: torch.Tensor,
    shift_range_per_dimension: torch.Tensor,
    num_shifts_per_system: torch.Tensor,
    max_shifts_per_system: int,
    half_fill: bool = False,
    wrap_positions: bool = True,
    positions_wrapped_buffer: torch.Tensor | None = None,
    per_atom_cell_offsets_buffer: torch.Tensor | None = None,
    inv_cell_buffer: torch.Tensor | None = None,
) -> None:
    """Compute two neighbor matrices with periodic boundary conditions using dual cutoffs.

    This function is torch compilable.

    See Also
    --------
    nvalchemiops.neighbors.naive_dual_cutoff.naive_neighbor_matrix_pbc_dual_cutoff : Core warp launcher
    naive_neighbor_list_dual_cutoff : High-level wrapper function
    """
    device = positions.device
    wp_vec_dtype = get_wp_vec_dtype(positions.dtype)
    wp_mat_dtype = get_wp_mat_dtype(positions.dtype)
    wp_dtype = get_wp_dtype(positions.dtype)

    wp_positions = wp.from_torch(
        positions, dtype=wp_vec_dtype, requires_grad=False, return_ctype=True
    )
    wp_cell = wp.from_torch(
        cell, dtype=wp_mat_dtype, requires_grad=False, return_ctype=True
    )
    wp_pbc = wp.from_torch(pbc, dtype=wp.bool, requires_grad=False, return_ctype=True)
    wp_shift_range = wp.from_torch(
        shift_range_per_dimension,
        dtype=wp.vec3i,
        requires_grad=False,
        return_ctype=True,
    )
    wp_neighbor_matrix1 = wp.from_torch(
        neighbor_matrix1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix2 = wp.from_torch(
        neighbor_matrix2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix_shifts1 = wp.from_torch(
        neighbor_matrix_shifts1, dtype=wp.vec3i, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix_shifts2 = wp.from_torch(
        neighbor_matrix_shifts2, dtype=wp.vec3i, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors1 = wp.from_torch(
        num_neighbors1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors2 = wp.from_torch(
        num_neighbors2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_positions_wrapped = (
        wp.from_torch(
            positions_wrapped_buffer,
            dtype=wp_vec_dtype,
            requires_grad=False,
            return_ctype=True,
        )
        if positions_wrapped_buffer is not None
        else None
    )
    wp_per_atom_cell_offsets = (
        wp.from_torch(
            per_atom_cell_offsets_buffer,
            dtype=wp.vec3i,
            requires_grad=False,
            return_ctype=True,
        )
        if per_atom_cell_offsets_buffer is not None
        else None
    )
    wp_inv_cell = (
        wp.from_torch(
            inv_cell_buffer, dtype=wp_mat_dtype, requires_grad=False, return_ctype=True
        )
        if inv_cell_buffer is not None
        else None
    )

    naive_neighbor_matrix_pbc_dual_cutoff(
        positions=wp_positions,
        cutoff1=cutoff1,
        cutoff2=cutoff2,
        cell=wp_cell,
        pbc=wp_pbc,
        shift_range=wp_shift_range,
        num_shifts=max_shifts_per_system,
        neighbor_matrix1=wp_neighbor_matrix1,
        neighbor_matrix2=wp_neighbor_matrix2,
        neighbor_matrix_shifts1=wp_neighbor_matrix_shifts1,
        neighbor_matrix_shifts2=wp_neighbor_matrix_shifts2,
        num_neighbors1=wp_num_neighbors1,
        num_neighbors2=wp_num_neighbors2,
        wp_dtype=wp_dtype,
        device=str(device),
        half_fill=half_fill,
        wrap_positions=wrap_positions,
        positions_wrapped_buffer=wp_positions_wrapped,
        per_atom_cell_offsets_buffer=wp_per_atom_cell_offsets,
        inv_cell_buffer=wp_inv_cell,
    )


@torch.library.custom_op(
    "nvalchemiops::_naive_neighbor_matrix_no_pbc_dual_cutoff_selective",
    mutates_args=(
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_matrix2",
        "num_neighbors2",
    ),
)
def _naive_neighbor_matrix_no_pbc_dual_cutoff_selective(
    positions: torch.Tensor,
    cutoff1: float,
    cutoff2: float,
    neighbor_matrix1: torch.Tensor,
    num_neighbors1: torch.Tensor,
    neighbor_matrix2: torch.Tensor,
    num_neighbors2: torch.Tensor,
    rebuild_flags: torch.Tensor,
    half_fill: bool = False,
) -> None:
    """Selective naive dual cutoff neighbor matrix custom op (no PBC).

    Wraps the GPU-side selective kernel: ``rebuild_flags[0]`` is checked on the
    device — no CPU-GPU synchronisation occurs.

    See Also
    --------
    nvalchemiops.neighbors.naive_dual_cutoff.naive_neighbor_matrix_dual_cutoff : Core warp launcher
    naive_neighbor_list_dual_cutoff : High-level wrapper that dispatches here when rebuild_flags is set
    """
    device = positions.device
    wp_device = wp.device_from_torch(device)
    wp_dtype = get_wp_dtype(positions.dtype)
    wp_vec_dtype = get_wp_vec_dtype(positions.dtype)

    wp_positions = wp.from_torch(
        positions, dtype=wp_vec_dtype, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix1 = wp.from_torch(
        neighbor_matrix1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors1 = wp.from_torch(
        num_neighbors1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix2 = wp.from_torch(
        neighbor_matrix2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors2 = wp.from_torch(
        num_neighbors2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_rebuild_flags = wp.from_torch(
        rebuild_flags.view(-1)[:1].contiguous(),
        dtype=wp.bool,
        requires_grad=False,
        return_ctype=True,
    )

    selective_zero_num_neighbors_single(
        wp_num_neighbors1, wp_rebuild_flags, str(wp_device)
    )
    selective_zero_num_neighbors_single(
        wp_num_neighbors2, wp_rebuild_flags, str(wp_device)
    )
    naive_neighbor_matrix_dual_cutoff(
        positions=wp_positions,
        cutoff1=cutoff1,
        cutoff2=cutoff2,
        neighbor_matrix1=wp_neighbor_matrix1,
        num_neighbors1=wp_num_neighbors1,
        neighbor_matrix2=wp_neighbor_matrix2,
        num_neighbors2=wp_num_neighbors2,
        wp_dtype=wp_dtype,
        device=str(wp_device),
        half_fill=half_fill,
        rebuild_flags=wp_rebuild_flags,
    )


@torch.library.custom_op(
    "nvalchemiops::_naive_neighbor_matrix_pbc_dual_cutoff_selective",
    mutates_args=(
        "neighbor_matrix1",
        "neighbor_matrix2",
        "neighbor_matrix_shifts1",
        "neighbor_matrix_shifts2",
        "num_neighbors1",
        "num_neighbors2",
    ),
)
def _naive_neighbor_matrix_pbc_dual_cutoff_selective(
    positions: torch.Tensor,
    cutoff1: float,
    cutoff2: float,
    cell: torch.Tensor,
    pbc: torch.Tensor,
    neighbor_matrix1: torch.Tensor,
    neighbor_matrix2: torch.Tensor,
    neighbor_matrix_shifts1: torch.Tensor,
    neighbor_matrix_shifts2: torch.Tensor,
    num_neighbors1: torch.Tensor,
    num_neighbors2: torch.Tensor,
    shift_range_per_dimension: torch.Tensor,
    num_shifts_per_system: torch.Tensor,
    max_shifts_per_system: int,
    rebuild_flags: torch.Tensor,
    half_fill: bool = False,
    wrap_positions: bool = True,
    positions_wrapped_buffer: torch.Tensor | None = None,
    per_atom_cell_offsets_buffer: torch.Tensor | None = None,
    inv_cell_buffer: torch.Tensor | None = None,
) -> None:
    """Selective naive dual cutoff PBC neighbor matrix custom op.

    ``rebuild_flags[0]`` is checked on the device — no CPU-GPU synchronisation occurs.

    See Also
    --------
    nvalchemiops.neighbors.naive_dual_cutoff.naive_neighbor_matrix_pbc_dual_cutoff : Core warp launcher
    naive_neighbor_list_dual_cutoff : High-level wrapper that dispatches here when rebuild_flags is set
    """
    device = positions.device
    wp_device = wp.device_from_torch(device)
    wp_vec_dtype = get_wp_vec_dtype(positions.dtype)
    wp_mat_dtype = get_wp_mat_dtype(positions.dtype)
    wp_dtype = get_wp_dtype(positions.dtype)

    wp_positions = wp.from_torch(
        positions, dtype=wp_vec_dtype, requires_grad=False, return_ctype=True
    )
    wp_cell = wp.from_torch(
        cell, dtype=wp_mat_dtype, requires_grad=False, return_ctype=True
    )
    wp_pbc = wp.from_torch(pbc, dtype=wp.bool, requires_grad=False, return_ctype=True)
    wp_shift_range = wp.from_torch(
        shift_range_per_dimension,
        dtype=wp.vec3i,
        requires_grad=False,
        return_ctype=True,
    )
    wp_neighbor_matrix1 = wp.from_torch(
        neighbor_matrix1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix2 = wp.from_torch(
        neighbor_matrix2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix_shifts1 = wp.from_torch(
        neighbor_matrix_shifts1, dtype=wp.vec3i, requires_grad=False, return_ctype=True
    )
    wp_neighbor_matrix_shifts2 = wp.from_torch(
        neighbor_matrix_shifts2, dtype=wp.vec3i, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors1 = wp.from_torch(
        num_neighbors1, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_num_neighbors2 = wp.from_torch(
        num_neighbors2, dtype=wp.int32, requires_grad=False, return_ctype=True
    )
    wp_rebuild_flags = wp.from_torch(
        rebuild_flags.view(-1)[:1].contiguous(),
        dtype=wp.bool,
        requires_grad=False,
        return_ctype=True,
    )
    wp_positions_wrapped = (
        wp.from_torch(
            positions_wrapped_buffer,
            dtype=wp_vec_dtype,
            requires_grad=False,
            return_ctype=True,
        )
        if positions_wrapped_buffer is not None
        else None
    )
    wp_per_atom_cell_offsets = (
        wp.from_torch(
            per_atom_cell_offsets_buffer,
            dtype=wp.vec3i,
            requires_grad=False,
            return_ctype=True,
        )
        if per_atom_cell_offsets_buffer is not None
        else None
    )
    wp_inv_cell = (
        wp.from_torch(
            inv_cell_buffer, dtype=wp_mat_dtype, requires_grad=False, return_ctype=True
        )
        if inv_cell_buffer is not None
        else None
    )

    selective_zero_num_neighbors_single(
        wp_num_neighbors1, wp_rebuild_flags, str(wp_device)
    )
    selective_zero_num_neighbors_single(
        wp_num_neighbors2, wp_rebuild_flags, str(wp_device)
    )
    naive_neighbor_matrix_pbc_dual_cutoff(
        positions=wp_positions,
        cutoff1=cutoff1,
        cutoff2=cutoff2,
        cell=wp_cell,
        pbc=wp_pbc,
        shift_range=wp_shift_range,
        num_shifts=max_shifts_per_system,
        neighbor_matrix1=wp_neighbor_matrix1,
        neighbor_matrix2=wp_neighbor_matrix2,
        neighbor_matrix_shifts1=wp_neighbor_matrix_shifts1,
        neighbor_matrix_shifts2=wp_neighbor_matrix_shifts2,
        num_neighbors1=wp_num_neighbors1,
        num_neighbors2=wp_num_neighbors2,
        wp_dtype=wp_dtype,
        device=str(wp_device),
        half_fill=half_fill,
        rebuild_flags=wp_rebuild_flags,
        wrap_positions=wrap_positions,
        positions_wrapped_buffer=wp_positions_wrapped,
        per_atom_cell_offsets_buffer=wp_per_atom_cell_offsets,
        inv_cell_buffer=wp_inv_cell,
    )


register_noop_fake(_naive_neighbor_matrix_no_pbc_dual_cutoff)
register_noop_fake(_naive_neighbor_matrix_pbc_dual_cutoff)
register_noop_fake(_naive_neighbor_matrix_no_pbc_dual_cutoff_selective)
register_noop_fake(_naive_neighbor_matrix_pbc_dual_cutoff_selective)


[docs] def naive_neighbor_list_dual_cutoff( positions: torch.Tensor, cutoff1: float, cutoff2: float, pbc: torch.Tensor | None = None, cell: torch.Tensor | None = None, max_neighbors1: int | None = None, max_neighbors2: int | None = None, half_fill: bool = False, fill_value: int | None = None, return_neighbor_list: bool = False, neighbor_matrix1: torch.Tensor | None = None, neighbor_matrix2: torch.Tensor | None = None, neighbor_matrix_shifts1: torch.Tensor | None = None, neighbor_matrix_shifts2: torch.Tensor | None = None, num_neighbors1: torch.Tensor | None = None, num_neighbors2: torch.Tensor | None = None, shift_range_per_dimension: torch.Tensor | None = None, num_shifts_per_system: torch.Tensor | None = None, max_shifts_per_system: int | None = None, rebuild_flags: torch.Tensor | None = None, wrap_positions: bool = True, positions_wrapped_buffer: torch.Tensor | None = None, per_atom_cell_offsets_buffer: torch.Tensor | None = None, inv_cell_buffer: torch.Tensor | None = None, ) -> ( tuple[ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, ] | tuple[ torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, ] | tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor] ): """Compute neighbor list using naive O(N^2) algorithm with dual cutoffs. Identifies all atom pairs within two different cutoff distances using a single brute-force pairwise distance calculation. This is more efficient than running two separate neighbor calculations when both neighbor lists are needed. Parameters ---------- positions : torch.Tensor, shape (N, 3) Atomic positions in Cartesian space, where N is the number of atoms. cutoff1 : float Inner cutoff radius; pairs within this distance populate the first neighbor list. cutoff2 : float Outer cutoff radius; pairs within this distance populate the second neighbor list. Must satisfy ``cutoff2 >= cutoff1``. pbc : torch.Tensor, shape (1, 3) or (3,), dtype=bool, optional Periodic boundary condition flags along x, y, z. Pass ``None`` for free-space. cell : torch.Tensor, shape (1, 3, 3), optional Unit-cell matrix whose rows are lattice vectors in Cartesian coordinates. Required when ``pbc`` is not ``None``. max_neighbors1 : int, optional Maximum number of neighbors per atom for the inner cutoff list. Estimated automatically when ``None`` and pre-allocated buffers are not supplied. max_neighbors2 : int, optional Maximum number of neighbors per atom for the outer cutoff list. Defaults to ``max_neighbors1`` when ``None``. half_fill : bool, optional If ``True``, only the lower-triangular half of each neighbor matrix is filled. Default is ``False``. fill_value : int, optional Padding value written into unused neighbor slots. Defaults to ``N`` (i.e., one past the last valid atom index). return_neighbor_list : bool, optional If ``True``, convert each neighbor matrix to a COO-style neighbor list ``(neighbor_indices, neighbor_ptr)``. Incurs a masking step; prefer the matrix format when possible. Default is ``False``. neighbor_matrix1 : torch.Tensor, shape (N, max_neighbors1), dtype=int32, optional Pre-allocated buffer for inner-cutoff neighbor indices. Modified in-place. Allocated internally when ``None``. neighbor_matrix2 : torch.Tensor, shape (N, max_neighbors2), dtype=int32, optional Pre-allocated buffer for outer-cutoff neighbor indices. Modified in-place. Allocated internally when ``None``. neighbor_matrix_shifts1 : torch.Tensor, shape (N, max_neighbors1, 3), dtype=int32, optional Pre-allocated buffer for PBC image shift vectors of the inner list. Modified in-place. Only used when ``pbc`` is not ``None``. neighbor_matrix_shifts2 : torch.Tensor, shape (N, max_neighbors2, 3), dtype=int32, optional Pre-allocated buffer for PBC image shift vectors of the outer list. Modified in-place. Only used when ``pbc`` is not ``None``. num_neighbors1 : torch.Tensor, shape (N,), dtype=int32, optional Pre-allocated buffer for per-atom inner-cutoff neighbor counts. Modified in-place. num_neighbors2 : torch.Tensor, shape (N,), dtype=int32, optional Pre-allocated buffer for per-atom outer-cutoff neighbor counts. Modified in-place. shift_range_per_dimension : torch.Tensor, shape (3,), dtype=int32, optional Number of periodic image layers to search along each lattice direction. Computed automatically when ``None``. num_shifts_per_system : torch.Tensor, optional Total number of image shift vectors for each system. Computed automatically when ``None``. max_shifts_per_system : int, optional Maximum value in ``num_shifts_per_system``. Computed automatically when ``None``. rebuild_flags : torch.Tensor, shape (1,), dtype=bool, optional Device-side flag. When provided, the neighbor lists are only recomputed for the system if ``rebuild_flags[0]`` is ``True``; no CPU-GPU synchronisation occurs. Pass ``None`` to always rebuild. wrap_positions : bool, optional If ``True``, atomic positions are wrapped into the primary unit cell before the neighbor search. Default is ``True``. positions_wrapped_buffer : torch.Tensor, shape (N, 3), optional Pre-allocated buffer for wrapped positions. Allocated internally when ``None``. per_atom_cell_offsets_buffer : torch.Tensor, shape (N, 3), dtype=int32, optional Pre-allocated buffer for per-atom cell-image offsets. Allocated internally when ``None``. inv_cell_buffer : torch.Tensor, shape (1, 3, 3), optional Pre-allocated buffer for the inverse cell matrix. Allocated internally when ``None``. Returns ------- tuple The return type depends on ``pbc`` and ``return_neighbor_list``: **No PBC, return_neighbor_list=False** — 4-tuple: neighbor_matrix1 : torch.Tensor, shape (N, max_neighbors1), dtype=int32 Inner-cutoff neighbor indices; unused slots are filled with ``fill_value``. num_neighbors1 : torch.Tensor, shape (N,), dtype=int32 Number of inner-cutoff neighbors per atom. neighbor_matrix2 : torch.Tensor, shape (N, max_neighbors2), dtype=int32 Outer-cutoff neighbor indices; unused slots are filled with ``fill_value``. num_neighbors2 : torch.Tensor, shape (N,), dtype=int32 Number of outer-cutoff neighbors per atom. **No PBC, return_neighbor_list=True** — 4-tuple: neighbor_list1 : torch.Tensor, shape (E1,), dtype=int32 Flat array of inner-cutoff neighbor atom indices. neighbor_ptr1 : torch.Tensor, shape (N+1,), dtype=int32 CSR row pointers for ``neighbor_list1``. neighbor_list2 : torch.Tensor, shape (E2,), dtype=int32 Flat array of outer-cutoff neighbor atom indices. neighbor_ptr2 : torch.Tensor, shape (N+1,), dtype=int32 CSR row pointers for ``neighbor_list2``. **With PBC, return_neighbor_list=False** — 6-tuple: neighbor_matrix1 : torch.Tensor, shape (N, max_neighbors1), dtype=int32 Inner-cutoff neighbor indices. num_neighbors1 : torch.Tensor, shape (N,), dtype=int32 Inner-cutoff neighbor counts. neighbor_matrix_shifts1 : torch.Tensor, shape (N, max_neighbors1, 3), dtype=int32 PBC image shift vectors for the inner list. neighbor_matrix2 : torch.Tensor, shape (N, max_neighbors2), dtype=int32 Outer-cutoff neighbor indices. num_neighbors2 : torch.Tensor, shape (N,), dtype=int32 Outer-cutoff neighbor counts. neighbor_matrix_shifts2 : torch.Tensor, shape (N, max_neighbors2, 3), dtype=int32 PBC image shift vectors for the outer list. **With PBC, return_neighbor_list=True** — 6-tuple: neighbor_list1 : torch.Tensor, shape (E1,), dtype=int32 Flat inner-cutoff neighbor indices. neighbor_ptr1 : torch.Tensor, shape (N+1,), dtype=int32 CSR row pointers for ``neighbor_list1``. unit_shifts1 : torch.Tensor, shape (E1, 3), dtype=int32 PBC image shift vectors corresponding to ``neighbor_list1``. neighbor_list2 : torch.Tensor, shape (E2,), dtype=int32 Flat outer-cutoff neighbor indices. neighbor_ptr2 : torch.Tensor, shape (N+1,), dtype=int32 CSR row pointers for ``neighbor_list2``. unit_shifts2 : torch.Tensor, shape (E2, 3), dtype=int32 PBC image shift vectors corresponding to ``neighbor_list2``. See Also -------- :func:`nvalchemiops.neighbors.naive_dual_cutoff.naive_neighbor_matrix_dual_cutoff` : Core warp launcher (no PBC). :func:`nvalchemiops.neighbors.naive_dual_cutoff.naive_neighbor_matrix_pbc_dual_cutoff` : Core warp launcher (with PBC). :func:`nvalchemiops.torch.neighbors.naive.naive_neighbor_list` : Single cutoff version. """ if pbc is None and cell is not None: raise ValueError("If cell is provided, pbc must also be provided") if pbc is not None and cell is None: raise ValueError("If pbc is provided, cell must also be provided") if cell is not None: cell = cell if cell.ndim == 3 else cell.unsqueeze(0) if pbc is not None: pbc = pbc if pbc.ndim == 2 else pbc.unsqueeze(0) if fill_value is None: fill_value = positions.shape[0] if max_neighbors1 is None and ( neighbor_matrix1 is None or neighbor_matrix2 is None or (neighbor_matrix_shifts1 is None and pbc is not None) or (neighbor_matrix_shifts2 is None and pbc is not None) or num_neighbors1 is None or num_neighbors2 is None ): max_neighbors2 = estimate_max_neighbors(cutoff2) max_neighbors1 = max_neighbors2 if max_neighbors2 is None: max_neighbors2 = max_neighbors1 if neighbor_matrix1 is None: neighbor_matrix1 = torch.full( (positions.shape[0], max_neighbors1), fill_value, dtype=torch.int32, device=positions.device, ) elif rebuild_flags is None: neighbor_matrix1.fill_(fill_value) if num_neighbors1 is None: num_neighbors1 = torch.zeros( positions.shape[0], dtype=torch.int32, device=positions.device ) elif rebuild_flags is None: num_neighbors1.zero_() if neighbor_matrix2 is None: neighbor_matrix2 = torch.full( (positions.shape[0], max_neighbors2), fill_value, dtype=torch.int32, device=positions.device, ) elif rebuild_flags is None: neighbor_matrix2.fill_(fill_value) if num_neighbors2 is None: num_neighbors2 = torch.zeros( positions.shape[0], dtype=torch.int32, device=positions.device ) elif rebuild_flags is None: num_neighbors2.zero_() if pbc is not None: if neighbor_matrix_shifts1 is None: neighbor_matrix_shifts1 = torch.zeros( (positions.shape[0], max_neighbors1, 3), dtype=torch.int32, device=positions.device, ) elif rebuild_flags is None: neighbor_matrix_shifts1.zero_() if neighbor_matrix_shifts2 is None: neighbor_matrix_shifts2 = torch.zeros( (positions.shape[0], max_neighbors2, 3), dtype=torch.int32, device=positions.device, ) elif rebuild_flags is None: neighbor_matrix_shifts2.zero_() if ( max_shifts_per_system is None or num_shifts_per_system is None or shift_range_per_dimension is None ): shift_range_per_dimension, num_shifts_per_system, max_shifts_per_system = ( compute_naive_num_shifts(cell, cutoff2, pbc) ) if pbc is None: if rebuild_flags is not None: _naive_neighbor_matrix_no_pbc_dual_cutoff_selective( positions=positions, cutoff1=cutoff1, cutoff2=cutoff2, neighbor_matrix1=neighbor_matrix1, num_neighbors1=num_neighbors1, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, rebuild_flags=rebuild_flags, half_fill=half_fill, ) else: _naive_neighbor_matrix_no_pbc_dual_cutoff( positions=positions, cutoff1=cutoff1, cutoff2=cutoff2, neighbor_matrix1=neighbor_matrix1, num_neighbors1=num_neighbors1, neighbor_matrix2=neighbor_matrix2, num_neighbors2=num_neighbors2, half_fill=half_fill, ) if return_neighbor_list: neighbor_list1, neighbor_ptr1 = get_neighbor_list_from_neighbor_matrix( neighbor_matrix1, num_neighbors=num_neighbors1, fill_value=fill_value ) neighbor_list2, neighbor_ptr2 = get_neighbor_list_from_neighbor_matrix( neighbor_matrix2, num_neighbors=num_neighbors2, fill_value=fill_value ) return ( neighbor_list1, neighbor_ptr1, neighbor_list2, neighbor_ptr2, ) else: return ( neighbor_matrix1, num_neighbors1, neighbor_matrix2, num_neighbors2, ) else: if rebuild_flags is not None: _naive_neighbor_matrix_pbc_dual_cutoff_selective( positions=positions, cutoff1=cutoff1, cutoff2=cutoff2, cell=cell, pbc=pbc, neighbor_matrix1=neighbor_matrix1, neighbor_matrix2=neighbor_matrix2, neighbor_matrix_shifts1=neighbor_matrix_shifts1, neighbor_matrix_shifts2=neighbor_matrix_shifts2, num_neighbors1=num_neighbors1, num_neighbors2=num_neighbors2, shift_range_per_dimension=shift_range_per_dimension, num_shifts_per_system=num_shifts_per_system, max_shifts_per_system=max_shifts_per_system, rebuild_flags=rebuild_flags, half_fill=half_fill, wrap_positions=wrap_positions, positions_wrapped_buffer=positions_wrapped_buffer, per_atom_cell_offsets_buffer=per_atom_cell_offsets_buffer, inv_cell_buffer=inv_cell_buffer, ) else: _naive_neighbor_matrix_pbc_dual_cutoff( positions=positions, cutoff1=cutoff1, cutoff2=cutoff2, cell=cell, pbc=pbc, neighbor_matrix1=neighbor_matrix1, neighbor_matrix2=neighbor_matrix2, neighbor_matrix_shifts1=neighbor_matrix_shifts1, neighbor_matrix_shifts2=neighbor_matrix_shifts2, num_neighbors1=num_neighbors1, num_neighbors2=num_neighbors2, shift_range_per_dimension=shift_range_per_dimension, num_shifts_per_system=num_shifts_per_system, max_shifts_per_system=max_shifts_per_system, half_fill=half_fill, wrap_positions=wrap_positions, positions_wrapped_buffer=positions_wrapped_buffer, per_atom_cell_offsets_buffer=per_atom_cell_offsets_buffer, inv_cell_buffer=inv_cell_buffer, ) if return_neighbor_list: neighbor_list1, neighbor_ptr1, unit_shifts1 = ( get_neighbor_list_from_neighbor_matrix( neighbor_matrix1, num_neighbors=num_neighbors1, neighbor_shift_matrix=neighbor_matrix_shifts1, fill_value=fill_value, ) ) neighbor_list2, neighbor_ptr2, unit_shifts2 = ( get_neighbor_list_from_neighbor_matrix( neighbor_matrix2, num_neighbors=num_neighbors2, neighbor_shift_matrix=neighbor_matrix_shifts2, fill_value=fill_value, ) ) return ( neighbor_list1, neighbor_ptr1, unit_shifts1, neighbor_list2, neighbor_ptr2, unit_shifts2, ) else: return ( neighbor_matrix1, num_neighbors1, neighbor_matrix_shifts1, neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2, )