# 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.
"""JAX bindings for the batched cluster-pair tile neighbor list.
Mirrors :mod:`nvalchemiops.torch.neighbors.batch_cluster_tile`: the per-system
Morton sort + padded SoA gather happens in JAX, then the bbox reduction +
tile-pair enumeration and the final conversion (matrix / COO) run on the
Warp side via ``jax_callable`` callbacks.
Scope: triclinic-safe, float32, ``N >= 0``, variable per-system N.
"""
from __future__ import annotations
import functools
import jax
import jax.numpy as jnp
import numpy as np
import warp as wp
from warp.jax_experimental import GraphMode, jax_callable
from nvalchemiops.jax.neighbors._cluster_tile_preload import (
_preload_cluster_tile_build_kernel,
_preload_cluster_tile_coo_kernel,
_preload_cluster_tile_query_kernel,
)
from nvalchemiops.neighbors.cluster_tile import (
TILE_GROUP_SIZE,
)
from nvalchemiops.neighbors.cluster_tile import (
batch_build_cluster_tile_list as _warp_batch_build_cluster_tile_list,
)
from nvalchemiops.neighbors.cluster_tile import (
batch_query_cluster_tile as _warp_batch_query_cluster_tile,
)
from nvalchemiops.neighbors.cluster_tile import (
batch_query_cluster_tile_coo as _warp_batch_query_cluster_tile_coo,
)
from nvalchemiops.neighbors.cluster_tile import (
estimate_batch_cluster_tile_segments as _warp_estimate_batch_cluster_tile_segments,
)
from nvalchemiops.neighbors.cluster_tile import (
estimate_batch_max_tiles_per_group as _estimate_batch_max_tiles_per_group,
)
from nvalchemiops.neighbors.neighbor_utils import (
NeighborOverflowError,
estimate_max_neighbors,
)
__all__ = [
"TILE_GROUP_SIZE",
"estimate_batch_max_tiles_per_group",
"allocate_batch_cluster_tile_list",
"batch_cluster_tile_neighbor_list",
"batch_query_cluster_tile_coo",
"batch_query_cluster_tile",
"batch_build_cluster_tile_list",
"estimate_batch_cluster_tile_list_sizes",
"estimate_batch_cluster_tile_segments",
]
# =============================================================================
# Sizing helper (pure JAX, no Warp launches)
# =============================================================================
def _batch_tile_buffer_max_tiles_per_group(
positions, batch_ptr, cutoff, cell_batch
) -> int:
"""``max_tiles_per_group`` for the batched compact tile buffer.
The compact buffer is ``ngroup_total * min(ngroup_total, mtpg)``.
``batch_ptr`` is structural (always concrete), so per-system ``ngroup`` is
available. When ``positions`` and ``cell_batch`` are concrete we
density-size from each system's geometry and take the max. When traced
(e.g. ``grad`` w.r.t. cell, or ``jit``) we fall back to ``max_i ngroup_i``:
the compact capacity ``ngroup_total * max_i ngroup_i >= sum_i ngroup_i**2``
then bounds the upper-triangular maximum, so it can never overflow.
"""
counts = np.asarray(batch_ptr).reshape(-1)
per_sys = (counts[1:] - counts[:-1]).astype(np.int64)
ngroups = [(int(n) + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE for n in per_sys]
max_ng = max(ngroups) if ngroups else 1
cutoff_concrete = not isinstance(cutoff, jax.core.Tracer)
vols = _concrete_cell_batch_volumes(cell_batch)
empty_batch = len(per_sys) == 0 and vols == []
if (
isinstance(positions, jax.core.Tracer)
or vols is None
or empty_batch
or not cutoff_concrete
):
return max(max_ng, 1)
return estimate_batch_max_tiles_per_group(batch_ptr, cutoff, cell_batch)
def _concrete_cell_batch_volumes(cell_batch) -> list[float] | None:
"""Per-system ``abs(det(cell))`` if ``cell_batch`` is concrete, else None."""
try:
arr = np.asarray(cell_batch)
except Exception:
return None
if arr.ndim != 3 or arr.shape[1:] != (3, 3):
return None
return [float(abs(np.linalg.det(c))) for c in arr]
def _concrete_batch_ptr_values(batch_ptr) -> list[int] | None:
"""Host ``batch_ptr`` values if concrete, else None."""
try:
values = np.asarray(batch_ptr).reshape(-1)
except Exception:
return None
try:
return [int(v) for v in values]
except Exception:
return None
[docs]
def estimate_batch_max_tiles_per_group(
batch_ptr: jax.Array,
cutoff: float,
cell_batch: jax.Array,
*,
safety: float = 2.0,
floor: int = 256,
) -> int:
"""Estimate batched ``max_tiles_per_group`` from concrete per-system cells.
Parameters
----------
batch_ptr : jax.Array, shape (num_systems + 1,)
Concrete cumulative atom counts.
cutoff : float
Cartesian cutoff used for cluster-tile construction.
cell_batch : jax.Array, shape (num_systems, 3, 3)
Concrete per-system cell matrices.
safety : float, default 2.0
Multiplier on the volumetric estimate.
floor : int, default 256
Minimum returned value for batched compact buffers.
Returns
-------
int
Shared ``max_tiles_per_group`` for the batched compact tile buffer.
Raises
------
ValueError
If ``batch_ptr`` or ``cell_batch`` is traced / not host-concrete, or if
``cell_batch`` does not have shape ``(num_systems, 3, 3)``.
"""
ptr_values = _concrete_batch_ptr_values(batch_ptr)
if ptr_values is None:
raise ValueError(
"batch_ptr must be concrete to estimate batch max_tiles_per_group"
)
if len(ptr_values) < 2:
raise ValueError("batch_ptr must have length at least 2")
vols = _concrete_cell_batch_volumes(cell_batch)
if vols is None:
try:
arr = np.asarray(cell_batch)
except Exception as exc:
raise ValueError(
"cell_batch must be concrete to estimate batch max_tiles_per_group"
) from exc
if arr.ndim != 3 or arr.shape[1:] != (3, 3):
raise ValueError("cell_batch must have shape (num_systems, 3, 3)")
return _estimate_batch_max_tiles_per_group(
ptr_values,
cutoff,
vols,
safety=safety,
floor=floor,
)
[docs]
def estimate_batch_cluster_tile_list_sizes(
batch_ptr: jax.Array,
max_tiles_per_group: int = 256,
) -> tuple[int, int, int, int, int]:
"""Estimate allocation sizes for the batched tile neighbor list state.
Mirrors
:func:`nvalchemiops.torch.neighbors.batch_cluster_tile.estimate_batch_cluster_tile_list_sizes`.
Converting ``batch_ptr`` to NumPy synchronizes it to size static buffers;
cache the result if calling from a hot loop.
Parameters
----------
batch_ptr : jax.Array, shape (num_systems + 1,), dtype=int32
Cumulative atom counts across systems.
max_tiles_per_group : int, default 256
Per-group tile capacity used to size the compact tile buffer.
Returns
-------
n_padded : int
Total number of padded atom slots across all systems.
ngroup : int
Total number of tile groups (``n_padded // TILE_GROUP_SIZE``).
ngroup_padded : int
``ngroup`` rounded up to the next ``TILE_GROUP_SIZE`` boundary plus one
extra group for alignment.
max_tiles : int
Compact tile-buffer capacity (``ngroup * min(ngroup, max_tiles_per_group)``).
num_systems : int
Number of systems (``batch_ptr.shape[0] - 1``).
"""
try:
ptr_values = np.asarray(batch_ptr, dtype=np.int64).reshape(-1)
except Exception as exc:
raise ValueError(
"batch_ptr must be concrete to estimate batch cluster-tile sizes"
) from exc
if len(ptr_values) < 2:
raise ValueError("batch_ptr must have length at least 2")
num_systems = len(ptr_values) - 1
natom_per_system = ptr_values[1:] - ptr_values[:-1]
natom_padded_per_system = (
(natom_per_system + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE
) * TILE_GROUP_SIZE
n_padded = int(np.sum(natom_padded_per_system))
ngroup = n_padded // TILE_GROUP_SIZE
ngroup_padded = (
(ngroup + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE
) * TILE_GROUP_SIZE + TILE_GROUP_SIZE
max_tiles = ngroup * min(ngroup, max_tiles_per_group)
return n_padded, ngroup, ngroup_padded, max_tiles, num_systems
[docs]
def estimate_batch_cluster_tile_segments(
batch_ptr: jax.Array,
max_neighbors: int,
max_tiles_per_group: int = 256,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
"""Estimate fixed per-system tile and COO segment buffers.
Parameters
----------
batch_ptr : jax.Array, shape (num_systems + 1,), dtype=int32
Cumulative atom counts across systems.
max_neighbors : int
Per-atom neighbor capacity used to size the per-system COO segments.
max_tiles_per_group : int, default 256
Per-group tile capacity used by the segment estimator.
Returns
-------
tile_capacities : jax.Array, shape (num_systems,), dtype=int32
Per-system tile segment capacity.
tile_offsets : jax.Array, shape (num_systems + 1,), dtype=int32
CSR-style offsets into the flat tile buffer.
pair_capacities : jax.Array, shape (num_systems,), dtype=int32
Per-system COO pair segment capacity.
pair_offsets : jax.Array, shape (num_systems + 1,), dtype=int32
CSR-style offsets into the flat pair buffer.
See Also
--------
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.allocate_batch_cluster_tile_list` : Allocates zeroed persistent tile buffers sized from these segments.
"""
if int(batch_ptr.shape[0]) < 2:
raise ValueError("batch_ptr must have length at least 2")
tile_caps, tile_offsets, pair_caps, pair_offsets = (
_warp_estimate_batch_cluster_tile_segments(
batch_ptr,
max_neighbors=int(max_neighbors),
max_tiles_per_group=int(max_tiles_per_group),
)
)
try:
device = next(iter(batch_ptr.devices()))
except AttributeError:
device = batch_ptr.device() if callable(batch_ptr.device) else batch_ptr.device
return (
jax.device_put(jnp.asarray(tile_caps, dtype=jnp.int32), device),
jax.device_put(jnp.asarray(tile_offsets, dtype=jnp.int32), device),
jax.device_put(jnp.asarray(pair_caps, dtype=jnp.int32), device),
jax.device_put(jnp.asarray(pair_offsets, dtype=jnp.int32), device),
)
[docs]
def allocate_batch_cluster_tile_list(
batch_ptr: jax.Array,
max_neighbors: int,
*,
max_tiles_per_group: int = 256,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]:
"""Allocate zeroed persistent tile buffers for the selective rebuild path.
JAX counterpart of
:func:`nvalchemiops.torch.neighbors.batch_cluster_tile.allocate_batch_cluster_tile_list`.
The dense ``format="tile"`` build allocates its own state, but the
*selective* rebuild path of :func:`batch_cluster_tile_neighbor_list`
threads persistent tile buffers across steps and the caller must supply
them. ``tile_system`` in particular MUST be zero-initialized: the
segmented + batched query kernel reads ``tile_system[tile]`` for every
allocated slot (including unwritten gap slots) *before* bounds-guarding, so
an uninitialized buffer (e.g. ``jnp.empty``) can drive out-of-bounds
indexing. This helper guarantees the zeroing and sizes the buffers
consistently with :func:`estimate_batch_cluster_tile_segments`.
Parameters
----------
batch_ptr : jax.Array, shape (S + 1,), dtype=int32
Cumulative atom counts.
max_neighbors : int
Per-atom neighbor capacity; sizes the per-system tile segments.
max_tiles_per_group : int, default 256
Per-group tile cap used by the segment estimator.
Returns
-------
tuple of jax.Array
``(num_tiles, tile_row_group, tile_col_group, tile_system,
tile_counts, tile_offsets)``, all int32 on ``batch_ptr``'s backend.
Pass them to :func:`batch_cluster_tile_neighbor_list` as
``previous_num_tiles``, ``previous_tile_row_group``,
``previous_tile_col_group``, ``previous_tile_system``,
``previous_tile_counts``, and ``tile_offsets`` respectively.
"""
num_systems = int(batch_ptr.shape[0]) - 1
_tile_caps, tile_offsets, _pair_caps, _pair_offsets = (
estimate_batch_cluster_tile_segments(
batch_ptr,
max_neighbors=int(max_neighbors),
max_tiles_per_group=int(max_tiles_per_group),
)
)
max_tiles = int(tile_offsets[-1])
try:
device = next(iter(batch_ptr.devices()))
except AttributeError:
device = batch_ptr.device() if callable(batch_ptr.device) else batch_ptr.device
def _zeros(n: int) -> jax.Array:
return jax.device_put(jnp.zeros(n, dtype=jnp.int32), device)
return (
_zeros(1), # num_tiles
_zeros(max_tiles), # tile_row_group
_zeros(max_tiles), # tile_col_group
_zeros(max_tiles), # tile_system
_zeros(num_systems), # tile_counts
tile_offsets, # tile_offsets (already on device)
)
# =============================================================================
# Morton helpers (JAX, mirror the torch path)
# =============================================================================
def _spread_10bit(x: jax.Array) -> jax.Array:
"""Spread the low 10 bits of ``x`` across a 30-bit Morton code."""
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
def _per_atom_morton_codes(
positions: jax.Array,
batch_idx: jax.Array,
inv_cell_batch: jax.Array,
) -> jax.Array:
"""Compute per-atom 30-bit Morton code using the per-system inverse cell."""
inv_per_atom = inv_cell_batch[batch_idx]
# frac = positions @ inv^T per atom
frac = jnp.einsum("ij,ikj->ik", positions, inv_per_atom)
frac = frac - jnp.floor(frac)
bucket = jnp.clip((frac * 1024.0).astype(jnp.int32), 0, 1023)
ix, iy, iz = bucket[:, 0], bucket[:, 1], bucket[:, 2]
return (_spread_10bit(iz) << 2) | (_spread_10bit(iy) << 1) | _spread_10bit(ix)
def _batched_morton_sort_padded(
positions: jax.Array,
batch_idx: jax.Array,
batch_ptr: jax.Array,
inv_cell_batch: jax.Array,
n_padded: int,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]:
"""Per-system Morton sort into the padded SoA layout.
Returns ``(sorted_atom_index, sort_inv, sorted_pos_x, sorted_pos_y,
sorted_pos_z, batch_idx_sorted)`` plus the running padded prefix
``batch_ptr_padded`` (returned last in a 7-tuple actually — see code).
"""
N = positions.shape[0]
num_systems = inv_cell_batch.shape[0]
natom_per_system = batch_ptr[1:] - batch_ptr[:-1]
natom_padded_per_system = (
(natom_per_system + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE
) * TILE_GROUP_SIZE
batch_ptr_padded = jnp.concatenate(
[
jnp.zeros(1, dtype=jnp.int32),
jnp.cumsum(natom_padded_per_system, dtype=jnp.int32),
]
)
codes = _per_atom_morton_codes(positions, batch_idx, inv_cell_batch)
# Lexicographic (batch_idx, codes) sort via two stable passes — JAX
# disables int64 by default, so packing (batch_idx << 32 | codes) into
# an int64 key would silently truncate and break the per-system
# ordering for batches with multiple systems.
perm_morton = jnp.argsort(codes, stable=True)
bi_after_morton = batch_idx[perm_morton]
perm_outer = jnp.argsort(bi_after_morton, stable=True)
sorted_atom_index_real = perm_morton[perm_outer].astype(jnp.int32)
# Inverse permutation over real atoms.
sort_inv = jnp.zeros(N, dtype=jnp.int32)
sort_inv = sort_inv.at[sorted_atom_index_real].set(
jnp.arange(N, dtype=jnp.int32),
)
# Placement indices for each real atom in the padded layout.
batch_idx_sorted_real = batch_idx[sorted_atom_index_real]
within_system = jnp.arange(N, dtype=jnp.int32) - batch_ptr[batch_idx_sorted_real]
padded_slot = batch_ptr_padded[batch_idx_sorted_real] + within_system
# sorted_atom_index[k] = N is the padding sentinel.
sp = jnp.full((n_padded,), N, dtype=jnp.int32)
sp = sp.at[padded_slot].set(sorted_atom_index_real)
# System index for every padded slot.
batch_idx_sorted = jnp.repeat(
jnp.arange(num_systems, dtype=jnp.int32),
natom_padded_per_system.astype(jnp.int32),
total_repeat_length=n_padded,
)
# Padding slots duplicate each system's first real atom.
first_atom_per_system = batch_ptr[:-1]
position_index = jnp.where(
sp == N,
first_atom_per_system[batch_idx_sorted],
sp,
)
sorted_pos = positions[position_index]
return (
sp,
sort_inv,
jnp.asarray(sorted_pos[:, 0]),
jnp.asarray(sorted_pos[:, 1]),
jnp.asarray(sorted_pos[:, 2]),
batch_idx_sorted,
batch_ptr_padded,
)
# =============================================================================
# jax_callable wrappers
# =============================================================================
def _batch_build_cluster_tile_list_callback(
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
group_system: wp.array(dtype=wp.int32),
group_ptr: wp.array(dtype=wp.int32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
group_ctr_x: wp.array(dtype=wp.float32),
group_ctr_y: wp.array(dtype=wp.float32),
group_ctr_z: wp.array(dtype=wp.float32),
group_ext_x: wp.array(dtype=wp.float32),
group_ext_y: wp.array(dtype=wp.float32),
group_ext_z: wp.array(dtype=wp.float32),
num_tiles: wp.array(dtype=wp.int32),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
cutoff: wp.float32,
) -> None:
_warp_batch_build_cluster_tile_list(
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
group_system=group_system,
group_ptr=group_ptr,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
cutoff=float(cutoff),
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
group_ctr_x_buffer=group_ctr_x,
group_ctr_y_buffer=group_ctr_y,
group_ctr_z_buffer=group_ctr_z,
group_ext_x_buffer=group_ext_x,
group_ext_y_buffer=group_ext_y,
group_ext_z_buffer=group_ext_z,
)
def _batch_build_cluster_tile_list_selective_callback(
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
group_system: wp.array(dtype=wp.int32),
group_ptr: wp.array(dtype=wp.int32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
group_ctr_x: wp.array(dtype=wp.float32),
group_ctr_y: wp.array(dtype=wp.float32),
group_ctr_z: wp.array(dtype=wp.float32),
group_ext_x: wp.array(dtype=wp.float32),
group_ext_y: wp.array(dtype=wp.float32),
group_ext_z: wp.array(dtype=wp.float32),
num_tiles: wp.array(dtype=wp.int32),
tile_offsets: wp.array(dtype=wp.int32),
tile_counts: wp.array(dtype=wp.int32),
rebuild_flags: wp.array(dtype=wp.bool),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
cutoff: wp.float32,
) -> None:
"""jax_callable callback for selective batched tile enumeration."""
_warp_batch_build_cluster_tile_list(
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
group_system=group_system,
group_ptr=group_ptr,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
cutoff=float(cutoff),
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
group_ctr_x_buffer=group_ctr_x,
group_ctr_y_buffer=group_ctr_y,
group_ctr_z_buffer=group_ctr_z,
group_ext_x_buffer=group_ext_x,
group_ext_y_buffer=group_ext_y,
group_ext_z_buffer=group_ext_z,
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=tile_counts,
)
def _batch_query_cluster_tile_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
num_neighbors: wp.array(dtype=wp.int32),
neighbor_matrix_shifts: wp.array(dtype=wp.int32, ndim=3),
cutoff: wp.float32,
natom: wp.int32,
) -> None:
# No ``n_tiles`` scalar — kernel guards per-tile.
_warp_batch_query_cluster_tile(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
natom=int(natom),
neighbor_matrix=neighbor_matrix,
num_neighbors=num_neighbors,
neighbor_matrix_shifts=neighbor_matrix_shifts,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
)
def _batch_query_cluster_tile_selective_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_offsets: wp.array(dtype=wp.int32),
tile_counts: wp.array(dtype=wp.int32),
rebuild_flags: wp.array(dtype=wp.bool),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
num_neighbors: wp.array(dtype=wp.int32),
neighbor_matrix_shifts: wp.array(dtype=wp.int32, ndim=3),
cutoff: wp.float32,
natom: wp.int32,
) -> None:
"""jax_callable callback for selective batched tile-pair -> matrix conversion."""
_warp_batch_query_cluster_tile(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
natom=int(natom),
neighbor_matrix=neighbor_matrix,
num_neighbors=num_neighbors,
neighbor_matrix_shifts=neighbor_matrix_shifts,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=tile_counts,
)
def _batch_query_cluster_tile_dual_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
num_neighbors: wp.array(dtype=wp.int32),
neighbor_matrix_shifts: wp.array(dtype=wp.int32, ndim=3),
neighbor_matrix2: wp.array(dtype=wp.int32, ndim=2),
num_neighbors2: wp.array(dtype=wp.int32),
neighbor_matrix_shifts2: wp.array(dtype=wp.int32, ndim=3),
cutoff: wp.float32,
cutoff2: wp.float32,
natom: wp.int32,
) -> None:
"""jax_callable callback for batched dual-cutoff matrix conversion."""
_warp_batch_query_cluster_tile(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
cutoff2=float(cutoff2),
natom=int(natom),
neighbor_matrix=neighbor_matrix,
num_neighbors=num_neighbors,
neighbor_matrix_shifts=neighbor_matrix_shifts,
neighbor_matrix2=neighbor_matrix2,
num_neighbors2=num_neighbors2,
neighbor_matrix_shifts2=neighbor_matrix_shifts2,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
)
def _batch_query_cluster_tile_dual_selective_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_offsets: wp.array(dtype=wp.int32),
tile_counts: wp.array(dtype=wp.int32),
rebuild_flags: wp.array(dtype=wp.bool),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
num_neighbors: wp.array(dtype=wp.int32),
neighbor_matrix_shifts: wp.array(dtype=wp.int32, ndim=3),
neighbor_matrix2: wp.array(dtype=wp.int32, ndim=2),
num_neighbors2: wp.array(dtype=wp.int32),
neighbor_matrix_shifts2: wp.array(dtype=wp.int32, ndim=3),
cutoff: wp.float32,
cutoff2: wp.float32,
natom: wp.int32,
) -> None:
"""jax_callable callback for selective batched dual-cutoff conversion."""
_warp_batch_query_cluster_tile(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
cutoff2=float(cutoff2),
natom=int(natom),
neighbor_matrix=neighbor_matrix,
num_neighbors=num_neighbors,
neighbor_matrix_shifts=neighbor_matrix_shifts,
neighbor_matrix2=neighbor_matrix2,
num_neighbors2=num_neighbors2,
neighbor_matrix_shifts2=neighbor_matrix_shifts2,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=tile_counts,
)
def _batch_query_cluster_tile_pair_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
num_neighbors: wp.array(dtype=wp.int32),
neighbor_matrix_shifts: wp.array(dtype=wp.int32, ndim=3),
# ``neighbor_vectors`` is typed ``wp.array(dtype=wp.vec3f, ndim=2)`` so
# the JAX-side ``(N, M, 3)`` float32 buffer maps to a ``(N, M)`` array
# of ``vec3f`` — same convention as
# :func:`nvalchemiops.jax.neighbors.cluster_tile._query_cluster_tile_pair_callback`.
neighbor_vectors: wp.array(dtype=wp.vec3f, ndim=2),
neighbor_distances: wp.array(dtype=wp.float32, ndim=2),
cutoff: wp.float32,
natom: wp.int32,
) -> None:
# No ``n_tiles`` scalar — kernel guards per-tile.
_warp_batch_query_cluster_tile(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
natom=int(natom),
neighbor_matrix=neighbor_matrix,
num_neighbors=num_neighbors,
neighbor_matrix_shifts=neighbor_matrix_shifts,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
return_vectors=True,
return_distances=True,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
)
def _batch_query_cluster_tile_coo_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
pair_counter: wp.array(dtype=wp.int32),
coo_list: wp.array(dtype=wp.int32, ndim=2),
coo_shifts: wp.array(dtype=wp.int32, ndim=2),
cutoff: wp.float32,
natom: wp.int32,
max_pairs: wp.int32,
) -> None:
# No ``n_tiles`` scalar — kernel guards per-tile.
_warp_batch_query_cluster_tile_coo(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
natom=int(natom),
max_pairs=int(max_pairs),
pair_counter=pair_counter,
coo_list=coo_list,
coo_shifts=coo_shifts,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
)
def _batch_query_cluster_tile_coo_segmented_callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_offsets: wp.array(dtype=wp.int32),
tile_counts: wp.array(dtype=wp.int32),
rebuild_flags: wp.array(dtype=wp.bool),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
pair_counter: wp.array(dtype=wp.int32),
pair_offsets: wp.array(dtype=wp.int32),
pair_counts: wp.array(dtype=wp.int32),
coo_list: wp.array(dtype=wp.int32, ndim=2),
coo_shifts: wp.array(dtype=wp.int32, ndim=2),
cutoff: wp.float32,
natom: wp.int32,
max_pairs: wp.int32,
) -> None:
"""jax_callable callback for batched fixed-segment selective COO."""
_warp_batch_query_cluster_tile_coo(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
natom=int(natom),
max_pairs=int(max_pairs),
pair_counter=pair_counter,
coo_list=coo_list,
coo_shifts=coo_shifts,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=tile_counts,
pair_offsets=pair_offsets,
pair_counts=pair_counts,
)
_jax_batch_build_cluster_tile_list = jax_callable(
_batch_build_cluster_tile_list_callback,
num_outputs=10,
in_out_argnames=[
"group_ctr_x",
"group_ctr_y",
"group_ctr_z",
"group_ext_x",
"group_ext_y",
"group_ext_z",
"num_tiles",
"tile_row_group",
"tile_col_group",
"tile_system",
],
graph_mode=GraphMode.WARP,
)
_jax_batch_build_cluster_tile_list_selective = jax_callable(
_batch_build_cluster_tile_list_selective_callback,
num_outputs=11,
in_out_argnames=[
"group_ctr_x",
"group_ctr_y",
"group_ctr_z",
"group_ext_x",
"group_ext_y",
"group_ext_z",
"num_tiles",
"tile_counts",
"tile_row_group",
"tile_col_group",
"tile_system",
],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile = jax_callable(
_batch_query_cluster_tile_callback,
num_outputs=3,
in_out_argnames=["neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts"],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile_selective = jax_callable(
_batch_query_cluster_tile_selective_callback,
num_outputs=3,
in_out_argnames=["neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts"],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile_dual = jax_callable(
_batch_query_cluster_tile_dual_callback,
num_outputs=6,
in_out_argnames=[
"neighbor_matrix",
"num_neighbors",
"neighbor_matrix_shifts",
"neighbor_matrix2",
"num_neighbors2",
"neighbor_matrix_shifts2",
],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile_dual_selective = jax_callable(
_batch_query_cluster_tile_dual_selective_callback,
num_outputs=6,
in_out_argnames=[
"neighbor_matrix",
"num_neighbors",
"neighbor_matrix_shifts",
"neighbor_matrix2",
"num_neighbors2",
"neighbor_matrix_shifts2",
],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile_pair = jax_callable(
_batch_query_cluster_tile_pair_callback,
num_outputs=5,
in_out_argnames=[
"neighbor_matrix",
"num_neighbors",
"neighbor_matrix_shifts",
"neighbor_vectors",
"neighbor_distances",
],
graph_mode=GraphMode.WARP,
)
@functools.cache
def _get_jax_batch_cluster_tile_pair_fn_callable(pair_fn):
"""Build (and cache) a batched ``jax_callable`` closing over ``pair_fn`` for the
cluster-tile pair-output -> matrix kernel.
Batched analogue of
``cluster_tile._get_jax_cluster_tile_pair_fn_callable``: adds the ``pair_params``
input + ``pair_energies`` / ``pair_forces`` outputs. Cached by ``pair_fn``
identity; fp32-only.
"""
def _callback(
sorted_atom_index: wp.array(dtype=wp.int32),
sorted_pos_x: wp.array(dtype=wp.float32),
sorted_pos_y: wp.array(dtype=wp.float32),
sorted_pos_z: wp.array(dtype=wp.float32),
cell_batch: wp.array(dtype=wp.mat33f),
inv_cell_batch: wp.array(dtype=wp.mat33f),
num_tiles: wp.array(dtype=wp.int32),
tile_row_group: wp.array(dtype=wp.int32),
tile_col_group: wp.array(dtype=wp.int32),
tile_system: wp.array(dtype=wp.int32),
neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
num_neighbors: wp.array(dtype=wp.int32),
neighbor_matrix_shifts: wp.array(dtype=wp.int32, ndim=3),
neighbor_vectors: wp.array(dtype=wp.vec3f, ndim=2),
neighbor_distances: wp.array(dtype=wp.float32, ndim=2),
pair_params: wp.array(dtype=wp.float32, ndim=2),
pair_energies: wp.array(dtype=wp.float32, ndim=2),
pair_forces: wp.array(dtype=wp.vec3f, ndim=2),
cutoff: wp.float32,
natom: wp.int32,
) -> None:
_warp_batch_query_cluster_tile(
sorted_atom_index=sorted_atom_index,
sorted_pos_x=sorted_pos_x,
sorted_pos_y=sorted_pos_y,
sorted_pos_z=sorted_pos_z,
cell_batch=cell_batch,
inv_cell_batch=inv_cell_batch,
num_tiles=num_tiles,
tile_row_group=tile_row_group,
tile_col_group=tile_col_group,
tile_system=tile_system,
cutoff=float(cutoff),
natom=int(natom),
neighbor_matrix=neighbor_matrix,
num_neighbors=num_neighbors,
neighbor_matrix_shifts=neighbor_matrix_shifts,
wp_dtype=wp.float32,
device=str(sorted_pos_x.device),
return_vectors=True,
return_distances=True,
neighbor_vectors=neighbor_vectors,
neighbor_distances=neighbor_distances,
pair_fn=pair_fn,
pair_params=pair_params,
pair_energies=pair_energies,
pair_forces=pair_forces,
)
return jax_callable(
_callback,
num_outputs=7,
in_out_argnames=[
"neighbor_matrix",
"num_neighbors",
"neighbor_matrix_shifts",
"neighbor_vectors",
"neighbor_distances",
"pair_energies",
"pair_forces",
],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile_coo = jax_callable(
_batch_query_cluster_tile_coo_callback,
num_outputs=3,
in_out_argnames=["pair_counter", "coo_list", "coo_shifts"],
graph_mode=GraphMode.WARP,
)
_jax_batch_query_cluster_tile_coo_segmented = jax_callable(
_batch_query_cluster_tile_coo_segmented_callback,
num_outputs=4,
in_out_argnames=["pair_counter", "pair_counts", "coo_list", "coo_shifts"],
graph_mode=GraphMode.WARP,
)
# =============================================================================
# User-facing entry points
# =============================================================================
def _make_batch_idx(batch_ptr: jax.Array, total_atoms: int) -> jax.Array:
"""Build per-atom system index from a batch_ptr (cumulative atom counts)."""
num_systems = int(batch_ptr.shape[0]) - 1
natom_per_system = (batch_ptr[1:] - batch_ptr[:-1]).astype(jnp.int32)
return jnp.repeat(
jnp.arange(num_systems, dtype=jnp.int32),
natom_per_system,
total_repeat_length=int(total_atoms),
)
[docs]
def batch_build_cluster_tile_list(
positions: jax.Array,
cutoff: float,
cell_batch: jax.Array,
batch_ptr: jax.Array,
*,
rebuild_flags: jax.Array | None = None,
tile_offsets: jax.Array | None = None,
tile_counts: jax.Array | None = None,
num_tiles: jax.Array | None = None,
tile_row_group: jax.Array | None = None,
tile_col_group: jax.Array | None = None,
tile_system: jax.Array | None = None,
) -> tuple[jax.Array, ...]:
"""Build the batched tile neighbor list state.
Runs per-system Morton sort + padded SoA gather in JAX, then the
bbox reduction + tile-pair enumeration on the Warp side.
Parameters
----------
positions : jax.Array, shape (N, 3), dtype=float32
Concatenated atomic coordinates for all systems.
cutoff : float
Bbox cutoff distance.
cell_batch : jax.Array, shape (S, 3, 3), dtype=float32
Per-system unit cell matrices.
batch_ptr : jax.Array, shape (S + 1,), dtype=int32
Cumulative atom counts.
rebuild_flags : jax.Array, shape (S,), dtype=bool, optional
Per-system rebuild flags. When provided, only systems with a True
flag have their tiles rebuilt. Requires ``tile_offsets`` and
``tile_counts``.
tile_offsets : jax.Array, shape (S + 1,), dtype=int32, optional
CSR-style per-system tile segment offsets. Required when
``rebuild_flags`` is provided.
tile_counts : jax.Array, shape (S,), dtype=int32, optional
Previous per-system tile counts. Required when ``rebuild_flags``
is provided; updated in-place for rebuilt systems.
num_tiles : jax.Array, shape (1,), dtype=int32, optional
Previous global tile count buffer. Allocated as zeros if None.
tile_row_group : jax.Array, shape (max_tiles,), dtype=int32, optional
Previous tile row-group index buffer. Allocated as zeros if None.
tile_col_group : jax.Array, shape (max_tiles,), dtype=int32, optional
Previous tile col-group index buffer. Allocated as zeros if None.
tile_system : jax.Array, shape (max_tiles,), dtype=int32, optional
Previous per-tile system index buffer. Allocated as zeros if None.
Returns
-------
tuple of jax.Array
19-element tuple ``(sorted_atom_index, sort_inv, sorted_pos_x,
sorted_pos_y, sorted_pos_z, batch_idx_sorted, batch_ptr_padded,
group_system, group_ptr, group_ctr_x, group_ctr_y, group_ctr_z,
group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group,
tile_col_group, tile_system)`` in the non-selective case, or a
20-element tuple with ``tile_counts`` appended when
``rebuild_flags`` is provided.
See Also
--------
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.batch_query_cluster_tile` : Converts the tile list to dense neighbor-matrix form.
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.batch_query_cluster_tile_coo` : Converts the tile list to flat COO form.
"""
if positions.dtype != jnp.float32:
raise TypeError(
"positions must be float32 (batch_cluster_tile kernels are float32 only)"
)
if cell_batch.dtype != jnp.float32:
raise TypeError("cell_batch must be float32")
if cell_batch.ndim != 3 or cell_batch.shape[1:] != (3, 3):
raise ValueError(
f"cell_batch must have shape (S, 3, 3); got {cell_batch.shape}"
)
if batch_ptr.dtype != jnp.int32:
batch_ptr = batch_ptr.astype(jnp.int32)
if int(batch_ptr.shape[0]) < 2:
raise ValueError("batch_ptr must have length at least 2")
# Geometry-size the compact tile buffer so dense/high-cutoff systems don't
# silently overflow; trace-safe ``max_i ngroup_i`` fallback when traced.
max_tiles_per_group = _batch_tile_buffer_max_tiles_per_group(
positions, batch_ptr, cutoff, cell_batch
)
n_padded, ngroup, ngroup_padded, max_tiles, _num_systems = (
estimate_batch_cluster_tile_list_sizes(
batch_ptr, max_tiles_per_group=max_tiles_per_group
)
)
inv_cell_batch = jnp.linalg.inv(cell_batch)
batch_idx = _make_batch_idx(batch_ptr, positions.shape[0])
(
sorted_atom_index,
sort_inv,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
batch_idx_sorted,
batch_ptr_padded,
) = _batched_morton_sort_padded(
positions,
batch_idx,
batch_ptr,
inv_cell_batch,
n_padded,
)
# Per-tile derived structure.
group_ptr = (batch_ptr_padded // TILE_GROUP_SIZE).astype(jnp.int32)
group_system = batch_idx_sorted[::TILE_GROUP_SIZE]
group_ctr_x = jnp.zeros(ngroup_padded, dtype=jnp.float32)
group_ctr_y = jnp.zeros(ngroup_padded, dtype=jnp.float32)
group_ctr_z = jnp.zeros(ngroup_padded, dtype=jnp.float32)
group_ext_x = jnp.zeros(ngroup_padded, dtype=jnp.float32)
group_ext_y = jnp.zeros(ngroup_padded, dtype=jnp.float32)
group_ext_z = jnp.zeros(ngroup_padded, dtype=jnp.float32)
if num_tiles is None:
num_tiles = jnp.zeros(1, dtype=jnp.int32)
if tile_row_group is None:
tile_row_group = jnp.zeros(max_tiles, dtype=jnp.int32)
if tile_col_group is None:
tile_col_group = jnp.zeros(max_tiles, dtype=jnp.int32)
if tile_system is None:
tile_system = jnp.zeros(max_tiles, dtype=jnp.int32)
if rebuild_flags is None:
_preload_cluster_tile_build_kernel(
batched=True,
segmented=False,
selective=False,
)
(
group_ctr_x,
group_ctr_y,
group_ctr_z,
group_ext_x,
group_ext_y,
group_ext_z,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
) = _jax_batch_build_cluster_tile_list(
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
group_system,
group_ptr,
cell_batch,
inv_cell_batch,
group_ctr_x,
group_ctr_y,
group_ctr_z,
group_ext_x,
group_ext_y,
group_ext_z,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
float(cutoff),
)
else:
if tile_offsets is None or tile_counts is None:
raise ValueError(
"rebuild_flags requires tile_offsets and tile_counts for "
"batched cluster_tile builds"
)
_preload_cluster_tile_build_kernel(
batched=True,
segmented=True,
selective=True,
)
rf = rebuild_flags.astype(jnp.bool_)
(
group_ctr_x,
group_ctr_y,
group_ctr_z,
group_ext_x,
group_ext_y,
group_ext_z,
num_tiles,
tile_counts,
tile_row_group,
tile_col_group,
tile_system,
) = _jax_batch_build_cluster_tile_list_selective(
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
group_system,
group_ptr,
cell_batch,
inv_cell_batch,
group_ctr_x,
group_ctr_y,
group_ctr_z,
group_ext_x,
group_ext_y,
group_ext_z,
num_tiles,
tile_offsets.astype(jnp.int32),
tile_counts.astype(jnp.int32),
rf,
tile_row_group,
tile_col_group,
tile_system,
float(cutoff),
)
del ngroup # implicit in group_system.shape[0]
result = (
sorted_atom_index,
sort_inv,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
batch_idx_sorted,
batch_ptr_padded,
group_system,
group_ptr,
group_ctr_x,
group_ctr_y,
group_ctr_z,
group_ext_x,
group_ext_y,
group_ext_z,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
)
if rebuild_flags is not None:
return (*result, tile_counts)
return result
[docs]
def batch_query_cluster_tile(
sorted_atom_index: jax.Array,
sorted_pos_x: jax.Array,
sorted_pos_y: jax.Array,
sorted_pos_z: jax.Array,
cell_batch: jax.Array,
num_tiles: jax.Array,
tile_row_group: jax.Array,
tile_col_group: jax.Array,
tile_system: jax.Array,
cutoff: float,
natom: int,
max_neighbors: int,
*,
fill_value: int | None = None,
cutoff2: float | None = None,
rebuild_flags: jax.Array | None = None,
tile_offsets: jax.Array | None = None,
tile_counts: jax.Array | None = None,
batch_idx: jax.Array | None = None,
neighbor_matrix: jax.Array | None = None,
num_neighbors: jax.Array | None = None,
neighbor_matrix_shifts: jax.Array | None = None,
neighbor_matrix2: jax.Array | None = None,
num_neighbors2: jax.Array | None = None,
neighbor_matrix_shifts2: jax.Array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: jax.Array | None = None,
neighbor_vectors: jax.Array | None = None,
neighbor_distances: jax.Array | None = None,
pair_energies: jax.Array | None = None,
pair_forces: jax.Array | None = None,
) -> tuple[jax.Array, ...]:
"""Convert the batched tile pair list to dense neighbor-matrix form.
Cluster-tile does not support partial neighbor lists; no
``target_indices`` kwarg. Pair-output kwargs are supported on the
eager-cutoff fp32 matrix path and rejected with ``cutoff2`` /
``rebuild_flags`` because those JAX cluster-tile paths are topology-only.
Parameters
----------
sorted_atom_index : jax.Array, shape (n_padded,), dtype=int32
Sorted (padded) atom indices from :func:`batch_build_cluster_tile_list`.
sorted_pos_x : jax.Array, shape (n_padded,), dtype=float32
X coordinates in Morton-sorted padded order.
sorted_pos_y : jax.Array, shape (n_padded,), dtype=float32
Y coordinates in Morton-sorted padded order.
sorted_pos_z : jax.Array, shape (n_padded,), dtype=float32
Z coordinates in Morton-sorted padded order.
cell_batch : jax.Array, shape (S, 3, 3), dtype=float32
Per-system unit cell matrices.
num_tiles : jax.Array, shape (1,), dtype=int32
Number of active tile pairs written by the build step.
tile_row_group : jax.Array, shape (max_tiles,), dtype=int32
Row group index for each tile pair.
tile_col_group : jax.Array, shape (max_tiles,), dtype=int32
Column group index for each tile pair.
tile_system : jax.Array, shape (max_tiles,), dtype=int32
System index for each tile pair.
cutoff : float
Neighbor search cutoff radius.
natom : int
Total number of real atoms across all systems.
max_neighbors : int
Per-atom neighbor capacity.
fill_value : int, optional
Sentinel written to unused neighbor slots. Defaults to ``natom``.
cutoff2 : float, optional
Second cutoff for dual-cutoff matrix output. Cannot be combined
with pair outputs.
rebuild_flags : jax.Array, shape (S,), dtype=bool, optional
Per-system selective rebuild flags. Requires ``tile_offsets``,
``tile_counts``, and ``batch_idx``.
tile_offsets : jax.Array, shape (S + 1,), dtype=int32, optional
Per-system tile segment offsets. Required with ``rebuild_flags``.
tile_counts : jax.Array, shape (S,), dtype=int32, optional
Per-system tile counts. Required with ``rebuild_flags``.
batch_idx : jax.Array, shape (natom,), dtype=int32, optional
Per-atom system index. Required with ``rebuild_flags``.
neighbor_matrix : jax.Array, shape (natom, max_neighbors), dtype=int32, optional
Previous neighbor-matrix buffer to update selectively.
num_neighbors : jax.Array, shape (natom,), dtype=int32, optional
Previous per-atom neighbor counts to update selectively.
neighbor_matrix_shifts : jax.Array, shape (natom, max_neighbors, 3), dtype=int32, optional
Previous per-pair image shifts to update selectively.
neighbor_matrix2 : jax.Array, shape (natom, max_neighbors), dtype=int32, optional
Second neighbor matrix for dual-cutoff output.
num_neighbors2 : jax.Array, shape (natom,), dtype=int32, optional
Per-atom counts for the second neighbor matrix.
neighbor_matrix_shifts2 : jax.Array, shape (natom, max_neighbors, 3), dtype=int32, optional
Image shifts for the second neighbor matrix.
return_vectors : bool, default False
If True, append per-pair displacement vectors. Matrix path only.
return_distances : bool, default False
If True, append per-pair scalar distances. Matrix path only.
pair_fn : wp.Function, optional
Inline Warp pair potential. Requires ``pair_params``.
pair_params : jax.Array, shape (natom, K), dtype=float32, optional
Per-atom parameters passed to ``pair_fn``.
neighbor_vectors : jax.Array, shape (natom, max_neighbors, 3), dtype=float32, optional
Pre-allocated buffer for displacement vectors.
neighbor_distances : jax.Array, shape (natom, max_neighbors), dtype=float32, optional
Pre-allocated buffer for scalar distances.
pair_energies : jax.Array, shape (natom, max_neighbors), dtype=float32, optional
Pre-allocated buffer for per-pair energies from ``pair_fn``.
pair_forces : jax.Array, shape (natom, max_neighbors, 3), dtype=float32, optional
Pre-allocated buffer for per-pair forces from ``pair_fn``.
Returns
-------
tuple of jax.Array
Base return is ``(neighbor_matrix, num_neighbors,
neighbor_matrix_shifts)``, shape ``(natom, max_neighbors)``,
``(natom,)``, and ``(natom, max_neighbors, 3)`` respectively.
With ``cutoff2``, a second triple is appended. With
``return_distances`` or ``return_vectors``, the corresponding
arrays are appended. With ``pair_fn``, ``(pair_energies,
pair_forces)`` are appended last.
See Also
--------
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.batch_build_cluster_tile_list` : Builds the tile list consumed by this function.
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.batch_query_cluster_tile_coo` : Alternative COO output from the same tile list.
"""
if pair_fn is not None and pair_params is None:
raise ValueError(
"pair_fn requires pair_params (a per-atom (n_atoms, K) parameter array).",
)
has_pair_outputs = (
bool(return_vectors) or bool(return_distances) or (pair_fn is not None)
)
dual_cutoff = cutoff2 is not None
selective = rebuild_flags is not None
if (dual_cutoff or selective) and has_pair_outputs:
raise NotImplementedError(
"JAX batch_cluster_tile cutoff2 / rebuild_flags are "
"matrix-topology features in this pass and cannot be combined "
"with return_distances or return_vectors.",
)
if selective:
if tile_offsets is None or tile_counts is None:
raise ValueError(
"rebuild_flags requires tile_offsets and tile_counts for "
"JAX batch_cluster_tile matrix queries"
)
if batch_idx is None:
raise ValueError("batch_idx is required when rebuild_flags is provided")
_preload_cluster_tile_query_kernel(
batched=True,
tile_segmented=selective,
selective=selective,
dual_cutoff=dual_cutoff,
return_vectors=has_pair_outputs,
return_distances=has_pair_outputs,
pair_fn=pair_fn,
)
if fill_value is None:
fill_value = natom
inv_cell_batch = jnp.linalg.inv(cell_batch)
if neighbor_matrix is None:
neighbor_matrix = jnp.zeros((natom, max_neighbors), dtype=jnp.int32)
elif not selective:
neighbor_matrix = neighbor_matrix.at[:].set(jnp.int32(0))
if num_neighbors is None:
num_neighbors = jnp.zeros(natom, dtype=jnp.int32)
elif not selective:
num_neighbors = num_neighbors.at[:].set(jnp.int32(0))
if neighbor_matrix_shifts is None:
neighbor_matrix_shifts = jnp.zeros((natom, max_neighbors, 3), dtype=jnp.int32)
elif not selective:
neighbor_matrix_shifts = neighbor_matrix_shifts.at[:].set(jnp.int32(0))
if selective:
rf = rebuild_flags.astype(jnp.bool_)
atom_rebuild = rf[batch_idx.astype(jnp.int32)]
num_neighbors = jnp.where(
atom_rebuild, jnp.zeros_like(num_neighbors), num_neighbors
)
else:
rf = None
if dual_cutoff:
if neighbor_matrix2 is None:
neighbor_matrix2 = jnp.zeros((natom, max_neighbors), dtype=jnp.int32)
elif not selective:
neighbor_matrix2 = neighbor_matrix2.at[:].set(jnp.int32(0))
if num_neighbors2 is None:
num_neighbors2 = jnp.zeros(natom, dtype=jnp.int32)
elif not selective:
num_neighbors2 = num_neighbors2.at[:].set(jnp.int32(0))
if neighbor_matrix_shifts2 is None:
neighbor_matrix_shifts2 = jnp.zeros(
(natom, max_neighbors, 3), dtype=jnp.int32
)
elif not selective:
neighbor_matrix_shifts2 = neighbor_matrix_shifts2.at[:].set(jnp.int32(0))
if selective:
num_neighbors2 = jnp.where(
atom_rebuild, jnp.zeros_like(num_neighbors2), num_neighbors2
)
# No host-side ``int(num_tiles[0])`` read: the warp kernel launches
# at the allocated tile-buffer capacity and early-returns per-tile
# using the device-side ``num_tiles`` array.
if has_pair_outputs:
if neighbor_vectors is None:
neighbor_vectors = jnp.zeros((natom, max_neighbors, 3), dtype=jnp.float32)
if neighbor_distances is None:
neighbor_distances = jnp.zeros((natom, max_neighbors), dtype=jnp.float32)
if pair_fn is not None:
if pair_energies is None:
pair_energies = jnp.zeros((natom, max_neighbors), dtype=jnp.float32)
if pair_forces is None:
pair_forces = jnp.zeros((natom, max_neighbors, 3), dtype=jnp.float32)
pair_params_arg = jnp.asarray(pair_params, dtype=jnp.float32)
pair_callable = _get_jax_batch_cluster_tile_pair_fn_callable(pair_fn)
(
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_vectors,
neighbor_distances,
pair_energies,
pair_forces,
) = pair_callable(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_vectors,
neighbor_distances,
pair_params_arg,
pair_energies,
pair_forces,
float(cutoff),
int(natom),
)
col_idx = jnp.arange(max_neighbors, dtype=jnp.int32)[jnp.newaxis, :]
active = col_idx < num_neighbors[:, jnp.newaxis]
neighbor_matrix = jnp.where(active, neighbor_matrix, jnp.int32(fill_value))
return (
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_vectors,
neighbor_distances,
pair_energies,
pair_forces,
)
(
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_vectors,
neighbor_distances,
) = _jax_batch_query_cluster_tile_pair(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_vectors,
neighbor_distances,
float(cutoff),
int(natom),
)
col_idx = jnp.arange(max_neighbors, dtype=jnp.int32)[jnp.newaxis, :]
active = col_idx < num_neighbors[:, jnp.newaxis]
neighbor_matrix = jnp.where(active, neighbor_matrix, jnp.int32(fill_value))
return (
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_vectors,
neighbor_distances,
)
if dual_cutoff and selective:
(
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_matrix2,
num_neighbors2,
neighbor_matrix_shifts2,
) = _jax_batch_query_cluster_tile_dual_selective(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_offsets.astype(jnp.int32),
tile_counts.astype(jnp.int32),
rf,
tile_row_group,
tile_col_group,
tile_system,
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_matrix2,
num_neighbors2,
neighbor_matrix_shifts2,
float(cutoff),
float(cutoff2),
int(natom),
)
elif dual_cutoff:
(
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_matrix2,
num_neighbors2,
neighbor_matrix_shifts2,
) = _jax_batch_query_cluster_tile_dual(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_matrix2,
num_neighbors2,
neighbor_matrix_shifts2,
float(cutoff),
float(cutoff2),
int(natom),
)
elif selective:
neighbor_matrix, num_neighbors, neighbor_matrix_shifts = (
_jax_batch_query_cluster_tile_selective(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_offsets.astype(jnp.int32),
tile_counts.astype(jnp.int32),
rf,
tile_row_group,
tile_col_group,
tile_system,
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
float(cutoff),
int(natom),
)
)
else:
neighbor_matrix, num_neighbors, neighbor_matrix_shifts = (
_jax_batch_query_cluster_tile(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
float(cutoff),
int(natom),
)
)
col_idx = jnp.arange(max_neighbors, dtype=jnp.int32)[jnp.newaxis, :]
active = col_idx < num_neighbors[:, jnp.newaxis]
neighbor_matrix = jnp.where(active, neighbor_matrix, jnp.int32(fill_value))
if dual_cutoff:
active2 = col_idx < num_neighbors2[:, jnp.newaxis]
neighbor_matrix2 = jnp.where(active2, neighbor_matrix2, jnp.int32(fill_value))
return (
neighbor_matrix,
num_neighbors,
neighbor_matrix_shifts,
neighbor_matrix2,
num_neighbors2,
neighbor_matrix_shifts2,
)
return neighbor_matrix, num_neighbors, neighbor_matrix_shifts
[docs]
def batch_query_cluster_tile_coo(
sorted_atom_index: jax.Array,
sorted_pos_x: jax.Array,
sorted_pos_y: jax.Array,
sorted_pos_z: jax.Array,
cell_batch: jax.Array,
num_tiles: jax.Array,
tile_row_group: jax.Array,
tile_col_group: jax.Array,
tile_system: jax.Array,
cutoff: float,
natom: int,
max_pairs: int,
*,
rebuild_flags: jax.Array | None = None,
tile_offsets: jax.Array | None = None,
tile_counts: jax.Array | None = None,
pair_offsets: jax.Array | None = None,
pair_counts: jax.Array | None = None,
neighbor_list: jax.Array | None = None,
neighbor_list_shifts: jax.Array | None = None,
) -> tuple[jax.Array, ...]:
"""Convert the batched tile pair list to flat COO form.
Parameters
----------
sorted_atom_index : jax.Array, shape (n_padded,), dtype=int32
Sorted (padded) atom indices from :func:`batch_build_cluster_tile_list`.
sorted_pos_x : jax.Array, shape (n_padded,), dtype=float32
X coordinates in Morton-sorted padded order.
sorted_pos_y : jax.Array, shape (n_padded,), dtype=float32
Y coordinates in Morton-sorted padded order.
sorted_pos_z : jax.Array, shape (n_padded,), dtype=float32
Z coordinates in Morton-sorted padded order.
cell_batch : jax.Array, shape (S, 3, 3), dtype=float32
Per-system unit cell matrices.
num_tiles : jax.Array, shape (1,), dtype=int32
Number of active tile pairs written by the build step.
tile_row_group : jax.Array, shape (max_tiles,), dtype=int32
Row group index for each tile pair.
tile_col_group : jax.Array, shape (max_tiles,), dtype=int32
Column group index for each tile pair.
tile_system : jax.Array, shape (max_tiles,), dtype=int32
System index for each tile pair.
cutoff : float
Neighbor search cutoff radius.
natom : int
Total number of real atoms across all systems.
max_pairs : int
Upper bound on the number of output pair entries.
rebuild_flags : jax.Array, shape (S,), dtype=bool, optional
Per-system selective rebuild flags. Requires ``tile_offsets``,
``tile_counts``, ``pair_offsets``, and ``pair_counts``.
tile_offsets : jax.Array, shape (S + 1,), dtype=int32, optional
Per-system tile segment offsets. Required with ``rebuild_flags``.
tile_counts : jax.Array, shape (S,), dtype=int32, optional
Per-system tile counts. Required with ``rebuild_flags``.
pair_offsets : jax.Array, shape (S + 1,), dtype=int32, optional
Per-system COO pair segment offsets for the segmented path.
Requires ``pair_counts``.
pair_counts : jax.Array, shape (S,), dtype=int32, optional
Previous per-system pair counts for selective update.
neighbor_list : jax.Array, shape (2, max_pairs), dtype=int32, optional
Pre-allocated COO pair buffer ``(i_idx, j_idx)`` for the
segmented path.
neighbor_list_shifts : jax.Array, shape (max_pairs, 3), dtype=int32, optional
Pre-allocated per-pair image shift buffer for the segmented path.
Returns
-------
tuple of jax.Array
Compact non-segmented path: ``(neighbor_list, neighbor_ptr,
coo_shifts)`` where ``neighbor_list`` has shape ``(2, npairs)``,
``neighbor_ptr`` is a CSR atom pointer of shape ``(natom + 1,)``,
and ``coo_shifts`` has shape ``(npairs, 3)``.
Segmented path (when ``pair_offsets`` is provided):
``(neighbor_list, pair_offsets, pair_counts, coo_shifts)`` where
``neighbor_list`` has shape ``(2, total_pairs)``.
See Also
--------
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.batch_build_cluster_tile_list` : Builds the tile list consumed by this function.
:func:`nvalchemiops.jax.neighbors.batch_cluster_tile.batch_query_cluster_tile` : Alternative dense matrix output from the same tile list.
"""
segmented = pair_offsets is not None or pair_counts is not None
if (pair_offsets is None) != (pair_counts is None):
raise ValueError("Pass both 'pair_offsets' and 'pair_counts', or neither.")
if (tile_offsets is None) != (tile_counts is None):
raise ValueError("Pass both 'tile_offsets' and 'tile_counts', or neither.")
if rebuild_flags is not None and not segmented:
raise ValueError("rebuild_flags requires pair_offsets and pair_counts")
if rebuild_flags is not None and (tile_offsets is None or tile_counts is None):
raise ValueError("rebuild_flags requires tile_offsets and tile_counts")
_preload_cluster_tile_coo_kernel(
batched=True,
tile_segmented=segmented,
coo_segmented=segmented,
selective=segmented,
)
inv_cell_batch = jnp.linalg.inv(cell_batch)
pair_counter = jnp.zeros(1, dtype=jnp.int32)
if segmented:
max_pairs = int(pair_offsets[-1])
if neighbor_list is None:
neighbor_list = jnp.zeros((2, max_pairs), dtype=jnp.int32)
if neighbor_list_shifts is None:
neighbor_list_shifts = jnp.zeros((max_pairs, 3), dtype=jnp.int32)
coo_list = neighbor_list.T.copy()
coo_shifts = neighbor_list_shifts
rf = (
rebuild_flags.astype(jnp.bool_)
if rebuild_flags is not None
else jnp.ones_like(pair_counts, dtype=jnp.bool_)
)
pair_counter, pair_counts, coo_list, coo_shifts = (
_jax_batch_query_cluster_tile_coo_segmented(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_offsets.astype(jnp.int32),
tile_counts.astype(jnp.int32),
rf,
tile_row_group,
tile_col_group,
tile_system,
pair_counter,
pair_offsets.astype(jnp.int32),
pair_counts.astype(jnp.int32),
coo_list,
coo_shifts,
float(cutoff),
int(natom),
int(max_pairs),
)
)
del pair_counter
return coo_list.T, pair_offsets, pair_counts, coo_shifts
coo_list = jnp.zeros((max_pairs, 2), dtype=jnp.int32)
coo_shifts = jnp.zeros((max_pairs, 3), dtype=jnp.int32)
pair_counter, coo_list, coo_shifts = _jax_batch_query_cluster_tile_coo(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch,
inv_cell_batch,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
pair_counter,
coo_list,
coo_shifts,
float(cutoff),
int(natom),
int(max_pairs),
)
npairs = int(pair_counter[0])
coo_list_trim = coo_list[:npairs]
coo_shifts_trim = coo_shifts[:npairs]
neighbor_list = coo_list_trim.T
per_atom = jnp.bincount(neighbor_list[0], length=natom).astype(jnp.int32)
neighbor_ptr = jnp.concatenate(
[
jnp.zeros(1, dtype=jnp.int32),
jnp.cumsum(per_atom, dtype=jnp.int32),
]
)
return neighbor_list, neighbor_ptr, coo_shifts_trim
[docs]
def batch_cluster_tile_neighbor_list(
positions: jax.Array,
cutoff: float,
cell_batch: jax.Array,
batch_ptr: jax.Array,
max_neighbors: int | None = None,
fill_value: int | None = None,
format: str = "matrix",
max_pairs: int | None = None,
*,
cutoff2: float | None = None,
rebuild_flags: jax.Array | None = None,
tile_offsets: jax.Array | None = None,
pair_offsets: jax.Array | None = None,
previous_tile_counts: jax.Array | None = None,
previous_pair_counts: jax.Array | None = None,
previous_neighbor_list: jax.Array | None = None,
previous_neighbor_list_shifts: jax.Array | None = None,
previous_num_tiles: jax.Array | None = None,
previous_tile_row_group: jax.Array | None = None,
previous_tile_col_group: jax.Array | None = None,
previous_tile_system: jax.Array | None = None,
previous_neighbor_matrix: jax.Array | None = None,
previous_num_neighbors: jax.Array | None = None,
previous_neighbor_matrix_shifts: jax.Array | None = None,
previous_neighbor_matrix2: jax.Array | None = None,
previous_num_neighbors2: jax.Array | None = None,
previous_neighbor_matrix_shifts2: jax.Array | None = None,
return_vectors: bool = False,
return_distances: bool = False,
pair_fn: wp.Function | None = None,
pair_params: jax.Array | None = None,
neighbor_vectors: jax.Array | None = None,
neighbor_distances: jax.Array | None = None,
pair_energies: jax.Array | None = None,
pair_forces: jax.Array | None = None,
) -> tuple[jax.Array, ...]:
"""Build a batched cluster-pair tile neighbor list (one-shot convenience).
Batched JAX binding for the cluster-pair tile algorithm. Per-system
Morton sort and padded SoA gather happen in JAX; bbox reduction,
tile-pair enumeration, and conversion (matrix / COO) run on the Warp
side via ``jax_callable`` callbacks. Mirrors
:func:`nvalchemiops.torch.neighbors.batch_cluster_tile.batch_cluster_tile_neighbor_list`.
Parameters
----------
positions : jax.Array, shape (total_atoms, 3), dtype=float32
Concatenated atomic coordinates across systems.
cutoff : float
Cutoff distance in Cartesian units. Must be positive.
cutoff2 : float, optional
Matrix-format second cutoff. Cannot be combined with pair outputs
or COO/tile formats.
rebuild_flags : jax.Array, shape (num_systems,), dtype=bool, optional
Selective rebuild flags for matrix or segmented COO output. Requires
fixed tile segments and previous output buffers.
cell_batch : jax.Array, shape (num_systems, 3, 3), dtype=float32
Per-system unit cell matrices. Cluster-tile assumes fully
periodic boundaries.
batch_ptr : jax.Array, shape (num_systems + 1,), dtype=int32
CSR pointer separating systems. Assumes positions are laid out
in system-contiguous order; interleaved layouts are **not
supported** and will silently emit cross-system pairs.
max_neighbors : int, optional
Max neighbors per atom (``"matrix"`` format only). Falls back to
:func:`estimate_max_neighbors`.
fill_value : int, optional
Matrix sentinel; defaults to ``total_atoms``.
format : {"matrix", "coo", "tile"}, default "matrix"
Output representation. See Returns.
max_pairs : int, optional
Upper bound for compact COO output; defaults to
``total_atoms * max_neighbors``.
tile_offsets, previous_tile_counts, pair_offsets, previous_pair_counts : jax.Array, optional
Fixed per-system segmented tile/COO buffers used with
``rebuild_flags``. Size them with
:func:`estimate_batch_cluster_tile_segments`.
previous_neighbor_list, previous_neighbor_list_shifts : jax.Array, optional
Fixed segmented-COO output buffers used with ``rebuild_flags`` and
``format="coo"``.
return_vectors, return_distances : bool, default False
If True, append per-pair displacement vectors / scalar distances
to the matrix-format return tuple. Matrix format only.
pair_fn, pair_params, neighbor_vectors, neighbor_distances, pair_energies, pair_forces : optional
Pair-output buffers and inline pair potential. Supported on the
eager-cutoff fp32 matrix/COO paths; rejected with ``cutoff2``,
``rebuild_flags``, or ``format="tile"``.
Returns
-------
For ``format == "matrix"``:
``(neighbor_matrix, num_neighbors, neighbor_matrix_shifts)``, with
optional ``(*, distances)`` and/or ``(*, vectors)`` appended when
``return_distances`` / ``return_vectors`` is True.
For ``format == "coo"``:
``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` in compact
mode, or ``(neighbor_list, pair_offsets, pair_counts,
neighbor_list_shifts, tile_offsets, tile_counts, num_tiles,
tile_row_group, tile_col_group, tile_system)`` with
``rebuild_flags`` segmented mode.
For ``format == "tile"``:
``(num_tiles, tile_row_group, tile_col_group, tile_system,
sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z,
batch_idx_sorted, batch_ptr_padded, group_ptr)`` — same 11-tuple
as the torch sibling so downstream tile consumers can be
backend-agnostic.
Notes
-----
- Cluster-tile is CUDA float32 only.
- Cluster-tile does not support partial neighbor lists (no
``target_indices`` kwarg).
- The unified :func:`nvalchemiops.jax.neighbors.neighbor_list` entry
point may select this binding automatically when the selector guards
and cost model prefer it; pass ``method="batch_cluster_tile"`` to
force it.
See Also
--------
nvalchemiops.jax.neighbors.cluster_tile_neighbor_list :
Single-system companion entry point.
nvalchemiops.jax.neighbors.batch_cluster_tile.batch_build_cluster_tile_list :
Lower-level build step exposed for caching across queries.
nvalchemiops.jax.neighbors.batch_cluster_tile.batch_query_cluster_tile :
Lower-level query step.
"""
from nvalchemiops.jax.neighbors._autograd import (
_build_index_residuals,
_NeighborForwardOutput,
_route_pair_outputs,
)
from nvalchemiops.jax.neighbors.neighbor_utils import (
coo_pack_pair_geometry,
get_neighbor_list_from_neighbor_matrix,
)
if format not in ("matrix", "coo", "tile"):
raise ValueError(
f"format must be 'matrix' | 'coo' | 'tile'; got {format!r}",
)
if pair_fn is not None and pair_params is None:
raise ValueError(
"pair_fn requires pair_params (a per-atom (n_atoms, K) parameter array).",
)
has_pair_outputs = (
bool(return_vectors) or bool(return_distances) or (pair_fn is not None)
)
dual_cutoff = cutoff2 is not None
selective = rebuild_flags is not None
if has_pair_outputs and format == "tile":
raise NotImplementedError(
"return_distances / return_vectors / pair_fn are not supported with "
"format='tile' on the JAX batch_cluster_tile binding; use 'matrix' or "
"'coo'.",
)
if dual_cutoff and format != "matrix":
raise NotImplementedError(
"JAX batch_cluster_tile cutoff2 is supported only with format='matrix'.",
)
if selective and format not in {"matrix", "coo"}:
raise NotImplementedError(
"JAX batch_cluster_tile rebuild_flags are supported only with "
"format='matrix' or segmented format='coo'.",
)
if (dual_cutoff or selective) and has_pair_outputs:
raise NotImplementedError(
"JAX batch_cluster_tile cutoff2 / rebuild_flags cannot be "
"combined with return_distances or return_vectors in this pass.",
)
if selective:
required = {
"tile_offsets": tile_offsets,
"previous_tile_counts": previous_tile_counts,
"previous_num_tiles": previous_num_tiles,
"previous_tile_row_group": previous_tile_row_group,
"previous_tile_col_group": previous_tile_col_group,
"previous_tile_system": previous_tile_system,
}
if format == "matrix":
required.update(
{
"previous_neighbor_matrix": previous_neighbor_matrix,
"previous_num_neighbors": previous_num_neighbors,
"previous_neighbor_matrix_shifts": previous_neighbor_matrix_shifts,
}
)
if dual_cutoff:
required.update(
{
"previous_neighbor_matrix2": previous_neighbor_matrix2,
"previous_num_neighbors2": previous_num_neighbors2,
"previous_neighbor_matrix_shifts2": previous_neighbor_matrix_shifts2,
}
)
elif format == "coo":
required.update(
{
"pair_offsets": pair_offsets,
"previous_pair_counts": previous_pair_counts,
"previous_neighbor_list": previous_neighbor_list,
"previous_neighbor_list_shifts": previous_neighbor_list_shifts,
}
)
missing = [name for name, value in required.items() if value is None]
if missing:
raise ValueError(
"rebuild_flags requires previous batch_cluster_tile state: "
+ ", ".join(missing)
)
if int(batch_ptr.shape[0]) < 2:
raise ValueError("batch_ptr must have length at least 2")
N = positions.shape[0]
if max_neighbors is None:
max_neighbors = estimate_max_neighbors(
cutoff2 if cutoff2 is not None else cutoff
)
if N == 0:
# Public docstring promises any ``N >= 0``. Empty batches would
# otherwise trip ``positions[-1:]`` in the JAX morton sort and
# zero-length ``jax.lax.dynamic_slice`` ops downstream.
nm0 = jnp.empty((0, int(max_neighbors)), dtype=jnp.int32)
nn0 = jnp.empty(0, dtype=jnp.int32)
ns0 = jnp.empty((0, int(max_neighbors), 3), dtype=jnp.int32)
if format == "coo":
coo_base = (
jnp.empty((2, 0), dtype=jnp.int32),
jnp.zeros(1, dtype=jnp.int32),
jnp.empty((0, 3), dtype=jnp.int32),
)
coo_tail: list = []
if return_distances:
coo_tail.append(jnp.empty(0, dtype=positions.dtype))
if return_vectors:
coo_tail.append(jnp.empty((0, 3), dtype=positions.dtype))
if pair_fn is not None:
coo_tail.extend(
(
jnp.empty(0, dtype=positions.dtype),
jnp.empty((0, 3), dtype=positions.dtype),
)
)
return (*coo_base, *coo_tail)
if format == "tile":
empty_i32 = jnp.empty(0, dtype=jnp.int32)
empty_f32 = jnp.empty(0, dtype=positions.dtype)
num_systems = int(batch_ptr.shape[0]) - 1
empty_bptr = jnp.zeros(num_systems + 1, dtype=jnp.int32)
empty_gptr = jnp.zeros(num_systems + 1, dtype=jnp.int32)
# 11-tuple matching the non-empty ``"tile"`` branch.
return (
jnp.zeros(1, dtype=jnp.int32), # num_tiles
empty_i32, # tile_row_group
empty_i32, # tile_col_group
empty_i32, # tile_system
empty_i32, # sorted_atom_index
empty_f32, # sorted_pos_x
empty_f32, # sorted_pos_y
empty_f32, # sorted_pos_z
empty_i32, # batch_idx_sorted
empty_bptr, # batch_ptr_padded
empty_gptr, # group_ptr
)
# format == "matrix"
if return_distances and return_vectors:
return (
nm0,
nn0,
ns0,
jnp.empty((0, int(max_neighbors)), dtype=positions.dtype),
jnp.empty((0, int(max_neighbors), 3), dtype=positions.dtype),
)
if return_distances:
return (
nm0,
nn0,
ns0,
jnp.empty((0, int(max_neighbors)), dtype=positions.dtype),
)
if return_vectors:
return (
nm0,
nn0,
ns0,
jnp.empty((0, int(max_neighbors), 3), dtype=positions.dtype),
)
if dual_cutoff:
matrix_out = (nm0, nn0, ns0, nm0, nn0, ns0)
else:
matrix_out = (nm0, nn0, ns0)
if selective:
return (
*matrix_out,
tile_offsets,
previous_tile_counts,
previous_num_tiles,
previous_tile_row_group,
previous_tile_col_group,
previous_tile_system,
)
return matrix_out
if has_pair_outputs:
if fill_value is None:
fill_value = N
batch_idx_arr = _make_batch_idx(batch_ptr, N).astype(jnp.int32)
def _forward(p: jax.Array, c: jax.Array) -> _NeighborForwardOutput:
p_det = jax.lax.stop_gradient(p)
c_det = jax.lax.stop_gradient(c)
(
sai,
_sort_inv,
spx,
spy,
spz,
_bis,
_bpp,
_gs,
_gp,
_gcx,
_gcy,
_gcz,
_gex,
_gey,
_gez,
nt,
trg,
tcg,
ts,
) = batch_build_cluster_tile_list(p_det, cutoff, c_det, batch_ptr)
out = batch_query_cluster_tile(
sai,
spx,
spy,
spz,
c_det,
nt,
trg,
tcg,
ts,
cutoff,
p_det.shape[0],
int(max_neighbors),
fill_value=int(fill_value),
return_vectors=True,
return_distances=True,
pair_fn=pair_fn,
pair_params=pair_params,
)
if pair_fn is not None:
nm, nn, shifts, vec, dist, pe, pf = out
else:
nm, nn, shifts, vec, dist = out
i_idx, j_idx, shifts_ret, _, mask_ = _build_index_residuals(
nm,
nn,
shifts,
)
K, M = nm.shape
extra_outputs = (
(nm, nn, shifts, pe, pf) if pair_fn is not None else (nm, nn, shifts)
)
return _NeighborForwardOutput(
distances=dist,
vectors=vec,
extra_outputs=extra_outputs,
i_idx=i_idx,
j_idx=j_idx,
shifts=shifts_ret,
batch_idx=batch_idx_arr,
active_mask=mask_,
matrix_shape=(K, M),
)
route_out = _route_pair_outputs(positions, cell_batch, _forward, {})
if pair_fn is not None:
(
distances_out,
vectors_out,
nm_out,
nn_out,
shifts_out,
pe_out,
pf_out,
) = route_out
else:
distances_out, vectors_out, nm_out, nn_out, shifts_out = route_out
pe_out = pf_out = None
if format == "coo":
# Mirror the batch_cell_list COO contract: convert the matrix topology
# to a flat neighbor list and repack the per-pair geometry (and pair_fn
# outputs) into the same COO order. Eager-only, like the matrix->COO
# index conversion (the pair count is data-dependent).
nl, nptr, nl_shifts = get_neighbor_list_from_neighbor_matrix(
nm_out,
num_neighbors=nn_out,
neighbor_shift_matrix=shifts_out,
fill_value=int(fill_value),
)
base = (nl, nptr, nl_shifts)
active = nm_out != int(fill_value)
distances_out, vectors_out = coo_pack_pair_geometry(
active, distances_out, vectors_out
)
if pair_fn is not None:
pe_out, pf_out = coo_pack_pair_geometry(active, pe_out, pf_out)
else:
base = (nm_out, nn_out, shifts_out)
# Return tail mirrors the torch contract: optional distances / vectors,
# then (pe, pf) when ``pair_fn`` is set.
tail: list = []
if return_distances:
tail.append(distances_out)
if return_vectors:
tail.append(vectors_out)
if pair_fn is not None:
tail.extend((pe_out, pf_out))
return (*base, *tail)
positions_topology = jax.lax.stop_gradient(positions)
cell_batch_topology = jax.lax.stop_gradient(cell_batch)
# Tile candidates must cover the larger radius so the cutoff2 matrix cannot
# miss pairs in the (cutoff, cutoff2] shell; the query filters each matrix
# by its own cutoff.
build_cutoff = cutoff if cutoff2 is None else max(float(cutoff), float(cutoff2))
build_out = batch_build_cluster_tile_list(
positions_topology,
build_cutoff,
cell_batch_topology,
batch_ptr,
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=previous_tile_counts,
num_tiles=previous_num_tiles,
tile_row_group=previous_tile_row_group,
tile_col_group=previous_tile_col_group,
tile_system=previous_tile_system,
)
if selective:
(
sorted_atom_index,
_sort_inv,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
batch_idx_sorted,
batch_ptr_padded,
_group_system,
group_ptr,
_group_ctr_x,
_group_ctr_y,
_group_ctr_z,
_group_ext_x,
_group_ext_y,
_group_ext_z,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
tile_counts,
) = build_out
else:
(
sorted_atom_index,
_sort_inv,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
batch_idx_sorted,
batch_ptr_padded,
_group_system,
group_ptr,
_group_ctr_x,
_group_ctr_y,
_group_ctr_z,
_group_ext_x,
_group_ext_y,
_group_ext_z,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
) = build_out
tile_counts = None
# Eager guard: raise on tile-buffer overflow instead of silently dropping
# tiles. Skipped under trace (the geometry fallback sizes the buffer so it
# can never overflow). Compact path checks the global ``num_tiles``;
# segmented (selective) checks per-system ``tile_counts``.
if not isinstance(num_tiles, jax.core.Tracer):
tile_capacity = int(tile_row_group.shape[0])
if tile_offsets is None:
n_tiles_host = int(num_tiles[0])
if n_tiles_host > tile_capacity:
raise NeighborOverflowError(tile_capacity, n_tiles_host)
elif tile_counts is not None and not isinstance(tile_counts, jax.core.Tracer):
counts_host = np.asarray(tile_counts).reshape(-1)
offs = np.asarray(tile_offsets).reshape(-1)
seg_caps = offs[1:] - offs[:-1]
over = np.nonzero(counts_host > seg_caps)[0]
if over.size > 0:
isys = int(over[0])
raise NeighborOverflowError(
int(seg_caps[isys]), int(counts_host[isys]), system_index=isys
)
if format == "tile":
# 11-tuple matching the torch sibling at
# ``nvalchemiops/torch/neighbors/batch_cluster_tile.py:batch_cluster_tile_neighbor_list``
# so downstream consumers can rely on a single shape across
# backends.
return (
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
batch_idx_sorted,
batch_ptr_padded,
group_ptr,
)
if format == "coo":
if selective:
coo_out = batch_query_cluster_tile_coo(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch_topology,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
cutoff,
N,
int(pair_offsets[-1]),
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=tile_counts,
pair_offsets=pair_offsets,
pair_counts=previous_pair_counts,
neighbor_list=previous_neighbor_list,
neighbor_list_shifts=previous_neighbor_list_shifts,
)
return (
*coo_out,
tile_offsets,
tile_counts,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
)
if max_pairs is None:
max_pairs = N * max_neighbors
return batch_query_cluster_tile_coo(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch_topology,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
cutoff,
N,
int(max_pairs),
)
matrix_out = batch_query_cluster_tile(
sorted_atom_index,
sorted_pos_x,
sorted_pos_y,
sorted_pos_z,
cell_batch_topology,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
cutoff,
N,
int(max_neighbors),
fill_value=fill_value,
cutoff2=cutoff2,
rebuild_flags=rebuild_flags,
tile_offsets=tile_offsets,
tile_counts=tile_counts,
batch_idx=_make_batch_idx(batch_ptr, N).astype(jnp.int32)
if selective
else None,
neighbor_matrix=previous_neighbor_matrix,
num_neighbors=previous_num_neighbors,
neighbor_matrix_shifts=previous_neighbor_matrix_shifts,
neighbor_matrix2=previous_neighbor_matrix2,
num_neighbors2=previous_num_neighbors2,
neighbor_matrix_shifts2=previous_neighbor_matrix_shifts2,
)
if selective:
return (
*matrix_out,
tile_offsets,
tile_counts,
num_tiles,
tile_row_group,
tile_col_group,
tile_system,
)
return matrix_out