Source code for nvalchemiops.jax.neighbors.cluster_tile

# SPDX-FileCopyrightText: Copyright (c) 2025 - 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""JAX bindings for the single-system cluster-pair tile neighbor list.

Mirrors :mod:`nvalchemiops.torch.neighbors.cluster_tile`: the Morton sort +
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.  Tile-format output returns the raw cluster
state for consumers that want to plug into shared-memory tile loads
directly.

Scope: single system, triclinic-safe, float32 only, ``N >= 0`` (any
non-32-aligned ``N`` is padded internally).
"""

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._autograd import (
    _build_index_residuals,
    _NeighborForwardOutput,
    _route_pair_outputs,
)
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.jax.neighbors.neighbor_utils import (
    coo_pack_pair_geometry,
    get_neighbor_list_from_neighbor_matrix,
)
from nvalchemiops.neighbors.cluster_tile import (
    TILE_GROUP_SIZE,
)
from nvalchemiops.neighbors.cluster_tile import (
    build_cluster_tile_list as _warp_build_cluster_tile_list,
)
from nvalchemiops.neighbors.cluster_tile import (
    estimate_max_tiles_per_group as _estimate_max_tiles_per_group,
)
from nvalchemiops.neighbors.cluster_tile import (
    query_cluster_tile as _warp_query_cluster_tile,
)
from nvalchemiops.neighbors.cluster_tile import (
    query_cluster_tile_coo as _warp_query_cluster_tile_coo,
)
from nvalchemiops.neighbors.neighbor_utils import (
    NeighborOverflowError,
    estimate_max_neighbors,
)

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


# =============================================================================
# Sizing + allocation helpers (pure JAX, no Warp launches)
# =============================================================================
def _concrete_cell_volume(cell) -> float | None:
    """Return ``abs(det(cell))`` if ``cell`` is concrete, else ``None``.

    Under ``jit``/``grad`` the cell may be a tracer; ``np.asarray`` raises and
    we return ``None`` so the caller falls back to a trace-safe static size.
    """
    try:
        arr = np.asarray(cell)
    except Exception:
        return None
    try:
        arr = arr.reshape(-1, 3, 3)[0] if arr.ndim == 3 else arr.reshape(3, 3)
        return float(abs(np.linalg.det(arr)))
    except Exception:
        return None


def _tile_buffer_max_tiles_per_group(positions, total_atoms: int, cutoff, cell) -> int:
    """``max_tiles_per_group`` for the JAX tile buffer.

    The tile-buffer size must be a *static* Python int (it's an array shape).
    When ``positions`` and ``cell`` are concrete (eager, or ``grad`` w.r.t.
    positions where the cell is a closure constant) we density-size from the
    geometry so dense/high-cutoff systems don't silently overflow.  When either
    is traced (e.g. ``grad`` w.r.t. cell, or ``jit`` of the whole call) the
    volume is unavailable, so fall back to ``ngroup`` -> capacity ``ngroup**2``,
    the upper-triangular maximum, which can *never* overflow (at the cost of
    more memory for very large traced systems).
    """
    ngroup = (int(total_atoms) + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE
    if ngroup <= 1:
        return 1
    cutoff_concrete = not isinstance(cutoff, jax.core.Tracer)
    vol = _concrete_cell_volume(cell)
    if isinstance(positions, jax.core.Tracer) or vol is None or not cutoff_concrete:
        return ngroup  # trace-safe upper-triangular cap (capacity ngroup**2)
    return _estimate_max_tiles_per_group(int(total_atoms), float(cutoff), vol)


[docs] def estimate_cluster_tile_list_sizes( total_atoms: int, max_tiles_per_group: int = 256, ) -> tuple[int, int, int, int]: """Estimate allocation sizes for the tile neighbor list state. Mirrors :func:`nvalchemiops.torch.neighbors.cluster_tile.estimate_cluster_tile_list_sizes`. Parameters ---------- total_atoms : int Real atom count. Any ``total_atoms >= 0`` is accepted. max_tiles_per_group : int, default 256 Upper bound on neighbor groups per row_group. Returns ------- n_padded : int Padded atom count = ``ceil(total_atoms / TILE_GROUP_SIZE) * TILE_GROUP_SIZE``. ngroup : int ``n_padded // TILE_GROUP_SIZE``. ngroup_padded : int Group-array pad length, multiple of TILE_GROUP_SIZE. max_tiles : int Upper bound on the tile-pair list size. """ if total_atoms < 0: raise ValueError(f"total_atoms must be >= 0; got {total_atoms}") n_padded = ( (total_atoms + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE if n_padded == 0: n_padded = TILE_GROUP_SIZE ngroup = n_padded // TILE_GROUP_SIZE ngroup_padded = ( (ngroup + TILE_GROUP_SIZE - 1) // TILE_GROUP_SIZE ) * TILE_GROUP_SIZE if ngroup_padded == ngroup: ngroup_padded = ngroup + TILE_GROUP_SIZE max_tiles = ngroup * min(ngroup, max_tiles_per_group) return n_padded, ngroup, ngroup_padded, max_tiles
def allocate_cluster_tile_list( total_atoms: int, dtype: jnp.dtype = jnp.float32, max_tiles_per_group: int = 256, ) -> tuple[jax.Array, ...]: """Allocate the state tensors consumed by :func:`build_cluster_tile_list`. Parameters ---------- total_atoms : int Real atom count. Any ``total_atoms >= 0`` is accepted. dtype : jnp.dtype, optional Floating-point dtype for position and bounding-box arrays. Defaults to ``jnp.float32``. max_tiles_per_group : int, optional Upper bound on neighbor groups per row_group; controls the ``tile_row_group`` / ``tile_col_group`` buffer capacity. Defaults to 256. Returns ------- tuple of jax.Array ``(sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group)`` — zero-initialised arrays with shapes and dtypes matching what :func:`build_cluster_tile_list` writes in-place. See Also -------- :func:`nvalchemiops.jax.neighbors.cluster_tile.build_cluster_tile_list` : Fills the allocated buffers given positions and a cutoff. :func:`nvalchemiops.jax.neighbors.cluster_tile.estimate_cluster_tile_list_sizes` : Returns the scalar sizes used here without allocating. """ n_padded, ngroup, ngroup_padded, max_tiles = estimate_cluster_tile_list_sizes( total_atoms, max_tiles_per_group=max_tiles_per_group, ) return ( jnp.zeros(n_padded, dtype=jnp.int32), # sorted_atom_index jnp.zeros(n_padded, dtype=jnp.int32), # morton_codes jnp.zeros(n_padded, dtype=dtype), # sorted_pos_x jnp.zeros(n_padded, dtype=dtype), # sorted_pos_y jnp.zeros(n_padded, dtype=dtype), # sorted_pos_z jnp.zeros(ngroup_padded, dtype=dtype), # group_ctr_x jnp.zeros(ngroup_padded, dtype=dtype), # group_ctr_y jnp.zeros(ngroup_padded, dtype=dtype), # group_ctr_z jnp.zeros(ngroup_padded, dtype=dtype), # group_ext_x jnp.zeros(ngroup_padded, dtype=dtype), # group_ext_y jnp.zeros(ngroup_padded, dtype=dtype), # group_ext_z jnp.zeros(1, dtype=jnp.int32), # num_tiles jnp.zeros(max_tiles, dtype=jnp.int32), # tile_row_group jnp.zeros(max_tiles, dtype=jnp.int32), # tile_col_group ) # ============================================================================= # Morton code 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 _compute_morton_codes( positions_padded: jax.Array, inv_cell: jax.Array, natom: int, n_padded: int, ) -> jax.Array: """Compute 30-bit Morton codes for atoms, sentinel-pad the tail. Pads positions are assumed to copy a real atom (in-cell coordinate); after Morton coding their 30-bit code is OR-ed with ``0x40000000`` so they sort after every real-atom code. """ inv_cell_2d = inv_cell[0] if inv_cell.ndim == 3 else inv_cell frac = positions_padded @ inv_cell_2d.T 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] codes = (_spread_10bit(iz) << 2) | (_spread_10bit(iy) << 1) | _spread_10bit(ix) if natom < n_padded: sentinel = jnp.full((n_padded - natom,), 0x40000000, dtype=jnp.int32) codes = jnp.concatenate([codes[:natom], sentinel]) return codes # ============================================================================= # jax_callable wrappers (Warp launchers behind GraphMode.WARP callbacks) # ============================================================================= def _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), cell: wp.array(dtype=wp.mat33f), inv_cell: 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), cutoff: wp.float32, ) -> None: """jax_callable callback for the SS tile bbox + tile-pair enumeration.""" _warp_build_cluster_tile_list( sorted_pos_x=sorted_pos_x, sorted_pos_y=sorted_pos_y, sorted_pos_z=sorted_pos_z, cell=cell, inv_cell=inv_cell, cutoff=float(cutoff), num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, 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 _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), cell: wp.array(dtype=wp.mat33f), inv_cell: 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), rebuild_flags: wp.array(dtype=wp.bool), cutoff: wp.float32, ) -> None: """jax_callable callback for selective SS tile enumeration.""" _warp_build_cluster_tile_list( sorted_pos_x=sorted_pos_x, sorted_pos_y=sorted_pos_y, sorted_pos_z=sorted_pos_z, cell=cell, inv_cell=inv_cell, cutoff=float(cutoff), num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, 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, ) def _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), 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 the tile-pair -> matrix conversion kernel. No ``n_tiles`` scalar — the warp launcher launches at the allocated ``tile_row_group`` capacity and guards per-tile via the device-side ``num_tiles`` array. """ _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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 _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), rebuild_flags: wp.array(dtype=wp.bool), 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 tile-pair -> matrix conversion.""" _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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, ) def _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), 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 dual-cutoff tile-pair -> matrix conversion.""" _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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 _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), rebuild_flags: wp.array(dtype=wp.bool), 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 dual-cutoff matrix conversion.""" _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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, ) def _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), 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), cutoff: wp.float32, natom: wp.int32, ) -> None: """jax_callable callback for the pair-output tile-pair -> matrix kernel. Same as :func:`_query_cluster_tile_callback` but with additional ``neighbor_vectors`` / ``neighbor_distances`` in/out arrays that the underlying Warp launcher fills with per-pair displacements and scalar distances when ``return_vectors`` / ``return_distances`` are enabled. Dtype contract: ``neighbor_vectors`` is typed ``wp.array(dtype=wp.vec3f, ndim=2)`` so that the JAX-side ``(N, M, 3)`` float32 buffer maps to a ``(N, M)`` array of ``vec3f``. This matches the convention used by every other neighbor-list kernel in this package (see ``neighbor_vectors: wp.array(dtype=vec_dtype, ndim=2)`` in :mod:`nvalchemiops.neighbors.naive.kernels` and the cell_list kernels). """ _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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 _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), 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: """jax_callable callback for the tile-pair -> flat COO conversion kernel. No ``n_tiles`` scalar — the warp launcher launches at the allocated ``tile_row_group`` capacity and guards per-tile via the device-side ``num_tiles`` array. """ _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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 _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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), rebuild_flags: wp.array(dtype=wp.bool), 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 fixed-segment selective COO conversion.""" _warp_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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, pair_offsets=pair_offsets, pair_counts=pair_counts, ) _jax_build_cluster_tile_list = jax_callable( _build_cluster_tile_list_callback, num_outputs=9, 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", ], graph_mode=GraphMode.WARP, ) _jax_build_cluster_tile_list_selective = jax_callable( _build_cluster_tile_list_selective_callback, num_outputs=9, 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", ], graph_mode=GraphMode.WARP, ) _jax_query_cluster_tile = jax_callable( _query_cluster_tile_callback, num_outputs=3, in_out_argnames=["neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts"], graph_mode=GraphMode.WARP, ) _jax_query_cluster_tile_selective = jax_callable( _query_cluster_tile_selective_callback, num_outputs=3, in_out_argnames=["neighbor_matrix", "num_neighbors", "neighbor_matrix_shifts"], graph_mode=GraphMode.WARP, ) _jax_query_cluster_tile_dual = jax_callable( _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_query_cluster_tile_dual_selective = jax_callable( _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_query_cluster_tile_pair = jax_callable( _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_cluster_tile_pair_fn_callable(pair_fn): """Build (and cache) a ``jax_callable`` that closes over ``pair_fn`` for the cluster-tile pair-output -> matrix kernel. Same as ``_jax_query_cluster_tile_pair`` but the callback closes over ``pair_fn`` (which cannot cross the JAX trace boundary as data) and adds the ``pair_params`` input + ``pair_energies`` / ``pair_forces`` outputs. Cached by ``pair_fn`` identity; one recompile per distinct ``pair_fn``. fp32-only, like the rest of the cluster-tile JAX binding. """ 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), num_tiles: wp.array(dtype=wp.int32), tile_row_group: wp.array(dtype=wp.int32), tile_col_group: wp.array(dtype=wp.int32), cell: wp.array(dtype=wp.mat33f), inv_cell: wp.array(dtype=wp.mat33f), 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_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, num_tiles=num_tiles, tile_row_group=tile_row_group, tile_col_group=tile_col_group, cell=cell, inv_cell=inv_cell, 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_query_cluster_tile_coo = jax_callable( _query_cluster_tile_coo_callback, num_outputs=3, in_out_argnames=["pair_counter", "coo_list", "coo_shifts"], graph_mode=GraphMode.WARP, ) _jax_query_cluster_tile_coo_segmented = jax_callable( _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 JAX entry points # ============================================================================= def _normalize_cell(cell: jax.Array, dtype: jnp.dtype) -> jax.Array: """Coerce ``(3, 3)`` or ``(1, 3, 3)`` cell to ``(1, 3, 3)`` with given dtype.""" if cell.ndim == 2: cell = cell[jnp.newaxis, :, :] if cell.shape != (1, 3, 3): raise ValueError(f"cell must have shape (3, 3) or (1, 3, 3); got {cell.shape}") return cell.astype(dtype) def _morton_sort_and_gather( positions: jax.Array, cell: jax.Array, n_padded: int, ) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array, jax.Array]: """Pad positions to ``n_padded``, compute Morton codes, argsort, gather SoA. Returns ``(sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z)``. """ N = positions.shape[0] inv_cell = jnp.linalg.inv(cell[0]) if N < n_padded: # Pad with the last real atom (any in-cell point works). pad = jnp.broadcast_to( positions[-1:], (n_padded - N, 3), ) padded_positions = jnp.concatenate([positions, pad], axis=0) else: padded_positions = positions morton_codes = _compute_morton_codes(padded_positions, inv_cell, N, n_padded) perm = jnp.argsort(morton_codes).astype(jnp.int32) sorted_pos = padded_positions[perm] return ( perm, morton_codes, jnp.asarray(sorted_pos[:, 0]), jnp.asarray(sorted_pos[:, 1]), jnp.asarray(sorted_pos[:, 2]), )
[docs] def build_cluster_tile_list( positions: jax.Array, cutoff: float, cell: jax.Array, *, rebuild_flags: jax.Array | None = None, num_tiles: jax.Array | None = None, tile_row_group: jax.Array | None = None, tile_col_group: jax.Array | None = None, ) -> tuple[ jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, jax.Array, ]: """Build the tile neighbor list state in pre-allocated form. Runs Morton sort + SoA gather in JAX, then the bbox reduction + tile enumeration on the Warp side via a ``jax_callable`` callback. Parameters ---------- positions : jax.Array, shape (N, 3), dtype=float32 Atomic coordinates. Any ``N >= 0``; non-32-aligned ``N`` is padded internally. cutoff : float Cutoff distance for the bbox filter. cell : jax.Array, shape (3, 3) or (1, 3, 3), dtype=float32 Unit cell matrix (orthorhombic or triclinic). Returns ------- tuple of jax.Array ``(sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group)``. Notes ----- Float32 only. The cluster-pair tile kernels currently only support ``wp.float32``. """ if positions.dtype != jnp.float32: raise TypeError( "positions must be float32 (cluster_tile kernels are float32 only)" ) N = positions.shape[0] # Geometry-size the tile buffer so dense/high-cutoff systems don't silently # overflow; trace-safe ``ngroup**2`` fallback when positions/cell are tracers. max_tiles_per_group = _tile_buffer_max_tiles_per_group(positions, N, cutoff, cell) n_padded, ngroup, ngroup_padded, max_tiles = estimate_cluster_tile_list_sizes( N, max_tiles_per_group=max_tiles_per_group ) cell_n = _normalize_cell(cell, jnp.float32) inv_cell_n = jnp.linalg.inv(cell_n[0])[jnp.newaxis, :, :] sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z = ( _morton_sort_and_gather(positions, cell_n, n_padded) ) # Allocate the bbox + tile output buffers. 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 rebuild_flags is None: _preload_cluster_tile_build_kernel( batched=False, 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, ) = _jax_build_cluster_tile_list( sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_n, inv_cell_n, 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, float(cutoff), ) else: _preload_cluster_tile_build_kernel( batched=False, segmented=False, selective=True, ) rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) ( 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, ) = _jax_build_cluster_tile_list_selective( sorted_pos_x, sorted_pos_y, sorted_pos_z, cell_n, inv_cell_n, 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, rf, float(cutoff), ) del ngroup # implicit in group_*_x.shape[0] return ( sorted_atom_index, morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, group_ctr_x, group_ctr_y, group_ctr_z, group_ext_x, group_ext_y, group_ext_z, num_tiles, tile_row_group, tile_col_group, )
[docs] def query_cluster_tile( sorted_atom_index: jax.Array, sorted_pos_x: jax.Array, sorted_pos_y: jax.Array, sorted_pos_z: jax.Array, num_tiles: jax.Array, tile_row_group: jax.Array, tile_col_group: jax.Array, cell: jax.Array, cutoff: float, natom: int, max_neighbors: int, *, fill_value: int | None = None, cutoff2: float | 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, rebuild_flags: 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 tile pair list to dense neighbor-matrix form. Skip-prefill: the Warp kernel only writes the active entries, then a JAX-side ``jnp.where`` fills unused tail columns with ``fill_value`` (defaults to ``natom``). Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. Pair-output kwargs (``return_vectors`` / ``return_distances`` / ``pair_fn`` and associated buffers) are supported on the eager-cutoff fp32 matrix path. They are rejected when combined with ``cutoff2`` or ``rebuild_flags`` because those JAX cluster-tile paths are topology-only. Parameters ---------- sorted_atom_index : jax.Array, shape (n_padded,), dtype=int32 Morton-sorted atom permutation produced by :func:`build_cluster_tile_list`. sorted_pos_x : jax.Array, shape (n_padded,), dtype=float32 Sorted x-coordinates (SoA layout). sorted_pos_y : jax.Array, shape (n_padded,), dtype=float32 Sorted y-coordinates (SoA layout). sorted_pos_z : jax.Array, shape (n_padded,), dtype=float32 Sorted z-coordinates (SoA layout). num_tiles : jax.Array, shape (1,), dtype=int32 Device-side count of active tile pairs. 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. cell : jax.Array, shape (3, 3) or (1, 3, 3), dtype=float32 Unit cell matrix. cutoff : float Primary neighbor cutoff radius. natom : int Real atom count (excluding padding). max_neighbors : int Maximum number of neighbors per atom; sets the column dimension of the output neighbor matrix. fill_value : int, optional Sentinel value written into unused neighbor slots. Defaults to ``natom``. cutoff2 : float, optional Second cutoff for dual-matrix output. Requires ``neighbor_matrix2`` / ``num_neighbors2`` / ``neighbor_matrix_shifts2`` output buffers. neighbor_matrix : jax.Array, shape (natom, max_neighbors), dtype=int32, optional Pre-allocated output buffer for neighbor indices. num_neighbors : jax.Array, shape (natom,), dtype=int32, optional Pre-allocated output buffer for per-atom neighbor counts. neighbor_matrix_shifts : jax.Array, shape (natom, max_neighbors, 3), dtype=int32, optional Pre-allocated output buffer for periodic image shift vectors. neighbor_matrix2 : jax.Array, shape (natom, max_neighbors), dtype=int32, optional Second neighbor-matrix buffer for dual-cutoff mode. num_neighbors2 : jax.Array, shape (natom,), dtype=int32, optional Second per-atom neighbor counts for dual-cutoff mode. neighbor_matrix_shifts2 : jax.Array, shape (natom, max_neighbors, 3), dtype=int32, optional Second shift buffer for dual-cutoff mode. rebuild_flags : jax.Array, shape (1,), dtype=bool, optional When set, skips atoms flagged as unchanged; requires all ``previous_*`` buffers to be passed. return_vectors : bool, optional If True, also fill and return per-pair displacement vectors. return_distances : bool, optional If True, also fill and return per-pair scalar distances. pair_fn : wp.Function, optional Warp kernel for inline pair-potential evaluation. pair_params : jax.Array, shape (natom, K), dtype=float32, optional Per-atom parameters forwarded to ``pair_fn``; required when ``pair_fn`` is set. neighbor_vectors : jax.Array, shape (natom, max_neighbors, 3), dtype=float32, optional Pre-allocated output buffer for displacement vectors. neighbor_distances : jax.Array, shape (natom, max_neighbors), dtype=float32, optional Pre-allocated output buffer for scalar distances. pair_energies : jax.Array, shape (natom, max_neighbors), dtype=float32, optional Pre-allocated output buffer for per-pair energies (``pair_fn`` path). pair_forces : jax.Array, shape (natom, max_neighbors, 3), dtype=float32, optional Pre-allocated output buffer for per-pair forces (``pair_fn`` path). Returns ------- tuple of jax.Array Base return is ``(neighbor_matrix, num_neighbors, neighbor_matrix_shifts)``. With ``cutoff2``, six arrays are returned: the base triple followed by ``(neighbor_matrix2, num_neighbors2, neighbor_matrix_shifts2)``. With ``return_vectors`` / ``return_distances`` / ``pair_fn``, the base is extended with the requested per-pair arrays in the order ``(*, neighbor_vectors, neighbor_distances)`` and optionally ``(*, pair_energies, pair_forces)``. See Also -------- :func:`nvalchemiops.jax.neighbors.cluster_tile.build_cluster_tile_list` : Builds the tile state consumed by this function. :func:`nvalchemiops.jax.neighbors.cluster_tile.query_cluster_tile_coo` : Alternative COO-format output from the same tile state. """ 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 cluster_tile cutoff2 / rebuild_flags are matrix-topology " "features in this pass and cannot be combined with " "return_distances or return_vectors.", ) _preload_cluster_tile_query_kernel( batched=False, tile_segmented=False, 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 cell_n = _normalize_cell(cell, jnp.float32) inv_cell_n = jnp.linalg.inv(cell_n[0])[jnp.newaxis, :, :] 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)) rf = None if selective: rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) num_neighbors = jnp.where(rf[0], jnp.zeros_like(num_neighbors), num_neighbors) if dual_cutoff and num_neighbors2 is not None: num_neighbors2 = jnp.where( rf[0], jnp.zeros_like(num_neighbors2), num_neighbors2 ) 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)) # 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: # pair_fn path: auto-allocate energy/force buffers and run the # call-time callable that closes over ``pair_fn`` (returns 7 outputs). 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_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, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, 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_query_cluster_tile_pair( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, 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_query_cluster_tile_dual_selective( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, rf, 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_query_cluster_tile_dual( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, 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_query_cluster_tile_selective( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, rf, neighbor_matrix, num_neighbors, neighbor_matrix_shifts, float(cutoff), int(natom), ) ) else: neighbor_matrix, num_neighbors, neighbor_matrix_shifts = ( _jax_query_cluster_tile( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, 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 query_cluster_tile_coo( sorted_atom_index: jax.Array, sorted_pos_x: jax.Array, sorted_pos_y: jax.Array, sorted_pos_z: jax.Array, num_tiles: jax.Array, tile_row_group: jax.Array, tile_col_group: jax.Array, cell: jax.Array, cutoff: float, natom: int, max_pairs: int, *, rebuild_flags: 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 tile pair list to flat COO form. In compact mode the pair count is data-dependent (eager-only). In segmented mode the output arrays have fixed shapes dictated by ``pair_offsets`` so the call is ``jit``-compatible. Parameters ---------- sorted_atom_index : jax.Array, shape (n_padded,), dtype=int32 Morton-sorted atom permutation from :func:`build_cluster_tile_list`. sorted_pos_x : jax.Array, shape (n_padded,), dtype=float32 Sorted x-coordinates (SoA). sorted_pos_y : jax.Array, shape (n_padded,), dtype=float32 Sorted y-coordinates (SoA). sorted_pos_z : jax.Array, shape (n_padded,), dtype=float32 Sorted z-coordinates (SoA). num_tiles : jax.Array, shape (1,), dtype=int32 Device-side count of active tile pairs. 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. cell : jax.Array, shape (3, 3) or (1, 3, 3), dtype=float32 Unit cell matrix. cutoff : float Neighbor cutoff radius. natom : int Real atom count (excluding padding). max_pairs : int Upper bound on the number of pair entries in compact mode. Ignored in segmented mode (derived from ``pair_offsets[-1]``). rebuild_flags : jax.Array, shape (1,), dtype=bool, optional Selective-rebuild flag. Requires ``pair_offsets`` and ``pair_counts`` (segmented mode only). pair_offsets : jax.Array, shape (ngroup + 1,), dtype=int32, optional CSR-style offsets into the fixed segmented pair buffer. Pass together with ``pair_counts`` to activate segmented mode. pair_counts : jax.Array, shape (ngroup,), dtype=int32, optional Per-group pair counts for the fixed segmented buffer. neighbor_list : jax.Array, shape (2, max_pairs), dtype=int32, optional Pre-allocated COO list buffer (segmented mode). neighbor_list_shifts : jax.Array, shape (max_pairs, 3), dtype=int32, optional Pre-allocated shift vector buffer (segmented mode). Returns ------- tuple of jax.Array Compact mode: ``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` where ``neighbor_list`` has shape ``(2, n_pairs)``, ``neighbor_ptr`` is the CSR row pointer of shape ``(natom + 1,)``, and ``neighbor_list_shifts`` has shape ``(n_pairs, 3)``. Segmented mode: ``(neighbor_list, pair_offsets, pair_counts, neighbor_list_shifts)`` where ``neighbor_list`` has shape ``(2, max_pairs)`` with fixed buffer size, and ``pair_counts`` reports the filled counts per group. See Also -------- :func:`nvalchemiops.jax.neighbors.cluster_tile.query_cluster_tile` : Dense neighbor-matrix output from the same tile state. :func:`nvalchemiops.jax.neighbors.cluster_tile.build_cluster_tile_list` : Builds the tile state consumed by this function. """ 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 rebuild_flags is not None and not segmented: raise ValueError("rebuild_flags requires pair_offsets and pair_counts") _preload_cluster_tile_coo_kernel( batched=False, tile_segmented=False, coo_segmented=segmented, selective=segmented, ) cell_n = _normalize_cell(cell, jnp.float32) inv_cell_n = jnp.linalg.inv(cell_n[0])[jnp.newaxis, :, :] 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 if rebuild_flags is None: rf = jnp.ones(1, dtype=jnp.bool_) else: rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) pair_counter, pair_counts, coo_list, coo_shifts = ( _jax_query_cluster_tile_coo_segmented( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, rf, 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_query_cluster_tile_coo( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_n, inv_cell_n, 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
def _cluster_tile_pair_outputs_forward( positions: jax.Array, cell: jax.Array, *, cutoff: float, max_neighbors: int, fill_value: int, pair_fn=None, pair_params: jax.Array | None = None, ) -> _NeighborForwardOutput: """Forward closure for the cluster_tile autograd path. Builds the cluster tiles + runs the pair-output query kernel inside a single closure. positions/cell are stop_gradient'd before any Warp launch; the autograd primitive's reconstruction backward gets the live tensors separately. When ``pair_fn`` is set, ``query_cluster_tile`` returns per-pair ``pair_energies`` / ``pair_forces`` which ride along in ``extra_outputs`` (forward-only). """ positions = jax.lax.stop_gradient(positions) cell = jax.lax.stop_gradient(cell) natom = positions.shape[0] ( sorted_atom_index, _morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, _group_ctr_x, _group_ctr_y, _group_ctr_z, _group_ext_x, _group_ext_y, _group_ext_z, num_tiles, tile_row_group, tile_col_group, ) = build_cluster_tile_list(positions, cutoff, cell) out = query_cluster_tile( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell, cutoff, natom, int(max_neighbors), fill_value=fill_value, return_vectors=True, return_distances=True, pair_fn=pair_fn, pair_params=pair_params, ) has_pair_fn = pair_fn is not None if has_pair_fn: 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 has_pair_fn 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=None, active_mask=mask_, matrix_shape=(K, M), )
[docs] def cluster_tile_neighbor_list( positions: jax.Array, cutoff: float, cell: 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, pair_offsets: 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_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 cluster-pair tile neighbor list (one-shot convenience). Single-system JAX binding for the cluster-pair tile algorithm. Runs Morton sort, bounding-box reduction, and tile enumeration, then emits the result in one of three formats selected by ``format=``. Mirrors :func:`nvalchemiops.torch.neighbors.cluster_tile.cluster_tile_neighbor_list`. Parameters ---------- positions : jax.Array, shape (N, 3), dtype=float32 Atomic coordinates. Any ``N >= 0``; non-32-aligned ``N`` is padded internally to a multiple of ``TILE_GROUP_SIZE``. 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 (1,), dtype=bool, optional Selective rebuild flag for matrix or segmented COO output. Requires previous tile state plus previous output buffers. cell : jax.Array, shape (3, 3) or (1, 3, 3), dtype=float32 Unit cell matrix. Cluster-tile assumes fully periodic boundaries. 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 ``N``. format : {"matrix", "coo", "tile"}, default "matrix" Output representation. See Returns. max_pairs : int, optional Upper bound for compact COO output; defaults to ``N * max_neighbors``. pair_offsets, previous_pair_counts, previous_neighbor_list, previous_neighbor_list_shifts : jax.Array, optional Fixed segmented-COO buffers used with ``rebuild_flags`` and ``format="coo"``. The return tuple preserves the fixed buffer shapes and reports the updated ``pair_counts``. 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, num_tiles, tile_row_group, tile_col_group)`` with ``rebuild_flags`` segmented mode. For ``format == "tile"``: ``(num_tiles, tile_row_group, tile_col_group, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z)`` --- the raw cluster state for downstream tile consumers. Notes ----- - Cluster-tile is CUDA float32 only. - Cluster-tile does not support partial neighbor lists; there is no ``target_indices`` kwarg. - The unified :func:`nvalchemiops.jax.neighbors.neighbor_list` entry point selects this binding automatically for fully-periodic float32 CUDA inputs with at least 2000 atoms and no pair-output kwargs; call this function directly to force the strategy. See Also -------- nvalchemiops.jax.neighbors.batch_cluster_tile_neighbor_list : Batched companion entry point. nvalchemiops.jax.neighbors.cluster_tile.build_cluster_tile_list : Lower-level build step exposed for caching across queries. nvalchemiops.jax.neighbors.cluster_tile.query_cluster_tile : Lower-level query step. """ if positions.dtype != jnp.float32: raise TypeError("positions must be float32") if format not in ("matrix", "coo", "tile"): raise ValueError( f"format must be 'matrix' | 'coo' | 'tile'; got {format!r}", ) if 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 cluster_tile binding; use 'matrix' or 'coo'.", ) if dual_cutoff and format != "matrix": raise NotImplementedError( "JAX cluster_tile cutoff2 is supported only with format='matrix'.", ) if selective and format not in {"matrix", "coo"}: raise NotImplementedError( "JAX 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 cluster_tile cutoff2 / rebuild_flags cannot be combined " "with return_distances or return_vectors in this pass.", ) if selective: required = { "previous_num_tiles": previous_num_tiles, "previous_tile_row_group": previous_tile_row_group, "previous_tile_col_group": previous_tile_col_group, } 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 cluster_tile state: " + ", ".join(missing) ) 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 ``N >= 0``. Returning zero-shaped # arrays of the right structure avoids the ``positions[-1:]`` # padding step in :func:`_morton_sort_and_gather` and gives # callers a no-op result that broadcasts naturally 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": # 7-tuple matching the non-empty ``"tile"`` branch shape. empty_i32 = jnp.empty(0, dtype=jnp.int32) empty_f32 = jnp.empty(0, dtype=positions.dtype) return ( jnp.zeros(1, dtype=jnp.int32), # num_tiles empty_i32, # tile_row_group empty_i32, # tile_col_group empty_i32, # sorted_atom_index empty_f32, # sorted_pos_x empty_f32, # sorted_pos_y empty_f32, # sorted_pos_z ) # 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, previous_num_tiles, previous_tile_row_group, previous_tile_col_group, ) return matrix_out if has_pair_outputs: if fill_value is None: fill_value = N forward_kwargs = { "cutoff": float(cutoff), "max_neighbors": int(max_neighbors), "fill_value": int(fill_value), "pair_fn": pair_fn, "pair_params": pair_params, } route_out = _route_pair_outputs( positions, cell, _cluster_tile_pair_outputs_forward, forward_kwargs, ) 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 naive / 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_topology = jax.lax.stop_gradient(cell) # 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)) ( sorted_atom_index, _morton_codes, sorted_pos_x, sorted_pos_y, sorted_pos_z, _group_ctr_x, _group_ctr_y, _group_ctr_z, _group_ext_x, _group_ext_y, _group_ext_z, num_tiles, tile_row_group, tile_col_group, ) = build_cluster_tile_list( positions_topology, build_cutoff, cell_topology, rebuild_flags=rebuild_flags, num_tiles=previous_num_tiles, tile_row_group=previous_tile_row_group, tile_col_group=previous_tile_col_group, ) # Eager guard: raise on tile-buffer overflow instead of silently dropping # tiles. Skipped under trace (positions/num_tiles are tracers) -- there the # ``ngroup**2`` geometry fallback guarantees the buffer never overflows. if not isinstance(num_tiles, jax.core.Tracer): n_tiles_host = int(num_tiles[0]) tile_capacity = int(tile_row_group.shape[0]) if n_tiles_host > tile_capacity: raise NeighborOverflowError(tile_capacity, n_tiles_host) if format == "tile": return ( num_tiles, tile_row_group, tile_col_group, sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, ) if format == "coo": if selective: coo_out = query_cluster_tile_coo( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_topology, cutoff, N, int(pair_offsets[-1]), rebuild_flags=rebuild_flags, pair_offsets=pair_offsets, pair_counts=previous_pair_counts, neighbor_list=previous_neighbor_list, neighbor_list_shifts=previous_neighbor_list_shifts, ) return (*coo_out, num_tiles, tile_row_group, tile_col_group) if max_pairs is None: max_pairs = N * max_neighbors return query_cluster_tile_coo( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_topology, cutoff, N, int(max_pairs), ) # format == "matrix" matrix_out = query_cluster_tile( sorted_atom_index, sorted_pos_x, sorted_pos_y, sorted_pos_z, num_tiles, tile_row_group, tile_col_group, cell_topology, cutoff, N, int(max_neighbors), fill_value=fill_value, cutoff2=cutoff2, rebuild_flags=rebuild_flags, 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, num_tiles, tile_row_group, tile_col_group) return matrix_out