# 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.
"""Private PyTorch neighbor-list dispatch helpers."""
from __future__ import annotations
from collections.abc import Iterable
import torch
import warp as wp
from nvalchemiops.neighbors.base_dispatch import (
FEATURE_BATCHED,
FEATURE_CUDA,
FEATURE_POSITIONS_FLOAT32,
neighbor_list_strategy_run_args,
optional_outputs_mask,
)
from nvalchemiops.neighbors.base_dispatch import (
estimate_neighbor_list_costs as _estimate_neighbor_list_costs_wp,
)
from nvalchemiops.torch.neighbors.neighbor_utils import (
_raise_if_compiling_host_only,
synthesize_cell_for_batch,
synthesize_cell_for_ss,
)
from nvalchemiops.torch.types import get_wp_mat_dtype
__all__ = [
"_auto_base_method_from_geometry",
"_auto_method_from_geometry",
"_reject_unsupported_cluster_tile_combo",
"_squeeze_single_system_cell_pbc",
"broadcast_shared_cell_for_batch",
"estimate_neighbor_list_costs",
"suggest_neighbor_list_method",
]
def _as_batched_cell(cell: torch.Tensor) -> torch.Tensor:
"""Return ``cell`` with an explicit leading system dimension."""
return cell if cell.ndim == 3 else cell.unsqueeze(0)
def _squeeze_single_system_cell_pbc(
cell: torch.Tensor | None,
pbc: torch.Tensor | None,
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
"""Convert single-system batched ``cell`` / ``pbc`` tensors to unbatched shape."""
if cell is not None and cell.ndim == 3 and cell.shape[0] == 1:
cell = cell[0]
if pbc is not None and pbc.ndim == 2 and pbc.shape[0] == 1:
pbc = pbc[0]
return cell, pbc
def broadcast_shared_cell_for_batch(
cell: torch.Tensor,
num_systems: int,
) -> torch.Tensor:
"""Broadcast a shared cell tensor to one cell per system for batched cluster-tile calls.
Parameters
----------
cell : torch.Tensor, shape (3, 3) or (num_systems, 3, 3)
Unit cell matrix. If shape is ``(3, 3)``, the single cell is expanded
to ``(num_systems, 3, 3)`` via contiguous broadcast. If already
``(num_systems, 3, 3)`` it is returned unchanged.
num_systems : int
Number of systems in the batch.
Returns
-------
torch.Tensor, shape (num_systems, 3, 3)
Contiguous per-system cell tensor.
"""
if cell.ndim == 3:
return cell
if cell.ndim != 2:
raise ValueError(
"cell for method='batch_cluster_tile' must have shape (3, 3) "
"or (num_systems, 3, 3)"
)
return cell.unsqueeze(0).expand(int(num_systems), -1, -1).contiguous()
def _selector_batch_ptr_from_geometry(
positions: torch.Tensor,
batch_idx: torch.Tensor | None,
batch_ptr: torch.Tensor | None,
num_systems: int,
) -> torch.Tensor:
"""Return an int32 batch pointer for selector metadata."""
if batch_ptr is not None:
return batch_ptr.to(dtype=torch.int32).detach().contiguous()
if batch_idx is not None:
counts = torch.bincount(batch_idx, minlength=int(num_systems)).to(torch.int32)
return torch.cat(
[
torch.zeros(1, dtype=torch.int32, device=positions.device),
counts.cumsum(dim=0),
]
)
base_count = positions.shape[0] // max(int(num_systems), 1)
counts = torch.full(
(int(num_systems),),
base_count,
dtype=torch.int32,
device=positions.device,
)
if num_systems > 0:
counts[0] += positions.shape[0] - base_count * int(num_systems)
return torch.cat(
[
torch.zeros(1, dtype=torch.int32, device=positions.device),
counts.cumsum(dim=0),
]
)
def _normalize_selector_cell_pbc(
cell: torch.Tensor,
pbc: torch.Tensor,
num_systems: int,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Return contiguous selector metadata with one cell per system."""
if cell.ndim == 2:
cell = cell.unsqueeze(0)
if cell.ndim != 3 or cell.shape[-2:] != (3, 3):
raise ValueError("cell must have shape (3, 3) or (num_systems, 3, 3)")
if cell.shape[0] == 1 and num_systems > 1:
cell = cell.expand(num_systems, -1, -1)
if cell.shape[0] != num_systems:
raise ValueError("cell must contain one matrix per system")
if pbc.ndim not in (1, 2):
raise ValueError("pbc must have shape (3,) or (num_systems, 3)")
if pbc.ndim == 2 and pbc.shape[0] == 1 and num_systems > 1:
pbc = pbc.expand(num_systems, -1)
return cell.detach().contiguous(), pbc.detach().to(dtype=torch.bool).contiguous()
[docs]
def estimate_neighbor_list_costs(
batch_ptr: torch.Tensor,
cell: torch.Tensor,
pbc: torch.Tensor,
cutoff: float,
*,
batch_idx: torch.Tensor | None = None,
max_nbins: int | None = None,
optional_outputs: Iterable[str] | None = None,
cutoff2: float | None = None,
half_fill: bool = False,
return_neighbor_list: bool = False,
target_indices: torch.Tensor | None = None,
return_vectors: bool = False,
return_distances: bool = False,
use_pair_fn: bool = False,
rebuild_flags: torch.Tensor | None = None,
wrap_positions: bool = True,
positions_dtype: torch.dtype | None = None,
) -> list[tuple[str, float]]:
"""Report feasible Torch neighbor-list strategies and their estimated cost.
Parameters
----------
batch_ptr : torch.Tensor, shape (num_systems + 1,), dtype=torch.int32
Cumulative atom counts. ``batch_ptr[-1]`` is the total atom count.
cell : torch.Tensor, shape (3, 3) or (num_systems, 3, 3)
Per-system cells, or one shared cell to broadcast.
pbc : torch.Tensor, shape (3,) or (num_systems, 3), dtype=bool
Shared or per-system PBC flags.
cutoff : float
Neighbor cutoff.
max_nbins : int, optional
Per-system cell-list cell cap. Defaults to the same cap used by the
active single-system or batched frontend.
optional_outputs : iterable of str, optional
Public neighbor-list option names to include in feasibility checks.
target_indices : torch.Tensor, optional
Public partial-row source indices. Its length is used to score
targeted naive/cell-list work.
positions_dtype : torch.dtype, optional
Position dtype used for feature feasibility. Standalone calls default
to ``cell.dtype``.
Returns
-------
list of (str, float)
Feasible strategies and their relative estimated cost (lower is
faster), sorted cheapest-first. Host-only: call outside
``torch.compile`` and pass the chosen name as an explicit ``method=``.
"""
_raise_if_compiling_host_only(
"estimate_neighbor_list_costs",
"Call it before compiling, then pass the selected strategy name as "
"an explicit method= argument to the compiled neighbor-list call.",
)
if batch_ptr.ndim != 1:
raise ValueError("batch_ptr must be a 1-D tensor")
if batch_ptr.shape[0] < 2:
raise ValueError("batch_ptr must have length at least 2")
num_systems = int(batch_ptr.shape[0]) - 1
cell, pbc = _normalize_selector_cell_pbc(cell, pbc, num_systems)
batch_ptr = batch_ptr.detach().to(dtype=torch.int32).contiguous()
if batch_idx is not None:
batch_idx = batch_idx.detach().to(dtype=torch.int32).contiguous()
wp_batch_ptr = wp.from_torch(batch_ptr, dtype=wp.int32, requires_grad=False)
wp_batch_idx = (
wp.from_torch(batch_idx, dtype=wp.int32, requires_grad=False)
if batch_idx is not None
else None
)
wp_cell = wp.from_torch(
cell, dtype=get_wp_mat_dtype(cell.dtype), requires_grad=False
)
wp_pbc = wp.from_torch(pbc, dtype=wp.bool, requires_grad=False)
feature_mask = 0
if cell.device.type == "cuda":
feature_mask |= FEATURE_CUDA
position_dtype = cell.dtype if positions_dtype is None else positions_dtype
if position_dtype == torch.float32:
feature_mask |= FEATURE_POSITIONS_FLOAT32
if num_systems > 1:
feature_mask |= FEATURE_BATCHED
options = optional_outputs_mask(
optional_outputs,
cutoff2=cutoff2,
half_fill=half_fill,
return_neighbor_list=return_neighbor_list,
target_indices=target_indices,
return_vectors=return_vectors,
return_distances=return_distances,
use_pair_fn=use_pair_fn,
rebuild_flags=rebuild_flags,
wrap_positions=wrap_positions,
)
return _estimate_neighbor_list_costs_wp(
wp_batch_ptr,
wp_cell,
wp_pbc,
cutoff,
batch_idx=wp_batch_idx,
max_nbins=max_nbins,
option_mask=options,
feature_mask=feature_mask,
target_count=(
int(target_indices.shape[0]) if target_indices is not None else None
),
)
[docs]
def suggest_neighbor_list_method(*args, **kwargs) -> str:
"""Return the cheapest feasible Torch neighbor-list strategy name.
Thin wrapper over :func:`nvalchemiops.torch.neighbors._dispatch.estimate_neighbor_list_costs`
that returns only the top-ranked strategy name. Accepts identical
positional and keyword arguments. Subject to the same host-only sync
caveat: do not call inside ``torch.compile``.
Returns
-------
str
Name of the lowest-cost feasible strategy, e.g. ``"cell_list_atom_centric"``
or ``"batch_cluster_tile"``.
See Also
--------
:func:`nvalchemiops.torch.neighbors._dispatch.estimate_neighbor_list_costs` : Returns full ranked list of strategies.
"""
return estimate_neighbor_list_costs(*args, **kwargs)[0][0]
def _auto_method_from_geometry(
positions: torch.Tensor,
cutoff: float,
cell: torch.Tensor | None,
pbc: torch.Tensor | None,
batch_idx: torch.Tensor | None,
batch_ptr: torch.Tensor | None,
num_systems: int,
*,
optional_outputs: Iterable[str] | None = None,
cutoff2: float | None = None,
half_fill: bool = False,
return_neighbor_list: bool = False,
target_indices: torch.Tensor | None = None,
return_vectors: bool = False,
return_distances: bool = False,
use_pair_fn: bool = False,
rebuild_flags: torch.Tensor | None = None,
wrap_positions: bool = True,
) -> str:
"""Return the cheapest base strategy name for the geometry (no ``batch_``).
The ``batch_`` prefix is added by the caller from ``num_systems``; this
returns the unbatched fine-grained name, e.g. ``"naive_tile"``.
"""
_raise_if_compiling_host_only(
"neighbor_list(method=None)",
"Choose the method before compiling with suggest_neighbor_list_method "
"or pass method='naive', method='cell_list', or another explicit "
"strategy name.",
)
if positions.shape[0] == 0:
return "cell_list_atom_centric"
selector_batch_ptr = _selector_batch_ptr_from_geometry(
positions, batch_idx, batch_ptr, num_systems
)
if cell is None:
if batch_idx is not None and num_systems > 1:
_, selector_cell, selector_pbc = synthesize_cell_for_batch(
positions, batch_idx, selector_batch_ptr, cutoff
)
else:
_, selector_cell, selector_pbc = synthesize_cell_for_ss(positions, cutoff)
if num_systems > 1:
selector_cell = selector_cell.expand(num_systems, -1, -1)
selector_pbc = selector_pbc.expand(num_systems, -1)
else:
if pbc is None:
raise ValueError("pbc is required when cell is provided")
selector_cell = cell
selector_pbc = pbc
name = suggest_neighbor_list_method(
selector_batch_ptr,
selector_cell,
selector_pbc,
cutoff,
batch_idx=batch_idx,
optional_outputs=optional_outputs,
cutoff2=cutoff2,
half_fill=half_fill,
return_neighbor_list=return_neighbor_list,
target_indices=target_indices,
return_vectors=return_vectors,
return_distances=return_distances,
use_pair_fn=use_pair_fn,
rebuild_flags=rebuild_flags,
wrap_positions=wrap_positions,
positions_dtype=positions.dtype,
)
return name[len("batch_") :] if name.startswith("batch_") else name
def _auto_base_method_from_geometry(
positions: torch.Tensor,
cutoff: float,
cell: torch.Tensor | None,
pbc: torch.Tensor | None,
batch_idx: torch.Tensor | None,
batch_ptr: torch.Tensor | None,
num_systems: int,
) -> str:
"""Pick the base method (``naive`` / ``cell_list`` / ``cluster_tile``)."""
name = _auto_method_from_geometry(
positions, cutoff, cell, pbc, batch_idx, batch_ptr, num_systems
)
return neighbor_list_strategy_run_args(name)[0]
def _reject_unsupported_cluster_tile_combo(
pbc: torch.Tensor | None,
half_fill: bool,
) -> None:
"""Raise ``NotImplementedError`` for unsupported explicit cluster-tile combos."""
if pbc is None:
raise NotImplementedError(
"method='cluster_tile' / 'batch_cluster_tile' is "
"fundamentally PBC-implicit; non-periodic systems (pbc=None) "
"are not supported. Use method='naive' or 'cell_list', "
"or pass a cell with fully periodic pbc."
)
try:
all_periodic = bool(pbc.all().item())
except RuntimeError:
all_periodic = True
if not all_periodic:
raise NotImplementedError(
"method='cluster_tile' / 'batch_cluster_tile' is "
"fundamentally PBC-implicit; pbc with any False entry "
"is not supported. Use method='naive' or 'cell_list'."
)
if half_fill:
raise NotImplementedError(
"method='cluster_tile' / 'batch_cluster_tile' uses "
"tile-level upper-triangular iteration; atom-level "
"half_fill=True is not supported. Use method='naive' or "
"'cell_list' for half-fill output."
)