Source code for nvalchemiops.jax.neighbors.batch_naive

# 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 batched naive O(N^2) neighbor list construction."""

from __future__ import annotations

import functools

import jax
import jax.numpy as jnp
import warp as wp
from warp.jax_experimental import GraphMode, jax_callable, jax_kernel

from nvalchemiops.jax.neighbors._autograd import (
    _build_index_residuals,
    _NeighborForwardOutput,
    _route_pair_outputs,
)
from nvalchemiops.jax.neighbors._dispatch import _is_jax_cpu_array
from nvalchemiops.jax.neighbors.neighbor_utils import (
    build_naive_kernel_tables,
    compute_naive_num_shifts,
    coo_pack_pair_geometry,
    get_neighbor_list_from_neighbor_matrix,
    prepare_batch_idx_ptr,
)
from nvalchemiops.neighbors.naive import (
    get_naive_neighbor_matrix_kernel as _get_naive_kernel,
)
from nvalchemiops.neighbors.naive.launchers import (
    _launch_naive_neighbor_matrix_no_pbc,
    _launch_naive_neighbor_matrix_pbc,
)
from nvalchemiops.neighbors.neighbor_utils import (
    estimate_max_neighbors,
    get_wrap_positions_kernel,
)

_DTYPE_TO_BATCH_NAIVE_KERNELS = (wp.float32, wp.float64)
(
    _fill_batch_naive_neighbor_matrix_kernels,
    _fill_batch_naive_neighbor_matrix_selective_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_selective_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_selective_kernels,
) = build_naive_kernel_tables(
    "single_cutoff", batched=True, dtypes=_DTYPE_TO_BATCH_NAIVE_KERNELS
)

(
    _fill_batch_naive_neighbor_matrix_half_kernels,
    _fill_batch_naive_neighbor_matrix_selective_half_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_half_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_selective_half_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_half_kernels,
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_selective_half_kernels,
) = build_naive_kernel_tables(
    "single_cutoff",
    batched=True,
    dtypes=_DTYPE_TO_BATCH_NAIVE_KERNELS,
    half_fill=True,
)

# Pair-output kernel tables (autograd path).  Same factory with
# return_vectors / return_distances flipped on.
#
# The PBC variant is hard-wired to ``pbc_mode='wrap_on_entry'``; the
# autograd path silently ignores the public ``wrap_positions`` kwarg
# (the kernel is idempotent on already-wrapped positions and correct on
# raw positions).

_fill_batch_naive_pair_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="none",
        batched=True,
        selective=False,
        return_vectors=True,
        return_distances=True,
    )
    for t in _DTYPE_TO_BATCH_NAIVE_KERNELS
}
_fill_batch_naive_pbc_pair_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="wrap_on_entry",
        batched=True,
        selective=False,
        return_vectors=True,
        return_distances=True,
    )
    for t in _DTYPE_TO_BATCH_NAIVE_KERNELS
}

# Half-fill specializations of the pair-output kernels (selected when
# ``half_fill=True``; ``half_fill`` is a compile-time constant in the factory).
_fill_batch_naive_pair_half_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="none",
        batched=True,
        selective=False,
        return_vectors=True,
        return_distances=True,
        half_fill=True,
    )
    for t in _DTYPE_TO_BATCH_NAIVE_KERNELS
}
_fill_batch_naive_pbc_pair_half_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="wrap_on_entry",
        batched=True,
        selective=False,
        return_vectors=True,
        return_distances=True,
        half_fill=True,
    )
    for t in _DTYPE_TO_BATCH_NAIVE_KERNELS
}


__all__ = ["batch_naive_neighbor_list"]

# ==============================================================================
# JAX Kernel Wrappers
# ==============================================================================

# No-PBC batch naive neighbor matrix kernel wrappers
_jax_fill_batch_naive_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)

# PBC batch naive neighbor matrix kernel wrappers
_jax_fill_batch_naive_pbc_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)

# Selective no-PBC batch naive neighbor matrix kernel wrappers
_jax_fill_batch_naive_selective_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_selective_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_selective_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_selective_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)

# Selective PBC batch naive neighbor matrix kernel wrappers
_jax_fill_batch_naive_pbc_selective_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_selective_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_selective_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_selective_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)

# Prewrapped PBC batch naive neighbor matrix kernel wrappers
_jax_fill_batch_naive_pbc_prewrapped_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_selective_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_selective_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_selective_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_selective_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)

# Half-fill batch naive neighbor matrix kernel wrappers
_jax_fill_batch_naive_half_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_half_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_half_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_half_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_half_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_half_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_half_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_half_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_selective_half_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_selective_half_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_selective_half_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_selective_half_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_selective_half_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_selective_half_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_selective_half_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_selective_half_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_half_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_half_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_half_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_half_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_selective_half_f32 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_selective_half_kernels[wp.float32],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_prewrapped_selective_half_f64 = jax_kernel(
    _fill_batch_naive_neighbor_matrix_pbc_prewrapped_selective_half_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)

# Pair-output variants (autograd path).
_jax_fill_batch_naive_pair_f32 = jax_kernel(
    _fill_batch_naive_pair_kernels[wp.float32],
    num_outputs=4,
    in_out_argnames=[
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_batch_naive_pair_f64 = jax_kernel(
    _fill_batch_naive_pair_kernels[wp.float64],
    num_outputs=4,
    in_out_argnames=[
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_pair_f32 = jax_kernel(
    _fill_batch_naive_pbc_pair_kernels[wp.float32],
    num_outputs=5,
    in_out_argnames=[
        "neighbor_matrix1",
        "neighbor_matrix_shifts1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_pair_f64 = jax_kernel(
    _fill_batch_naive_pbc_pair_kernels[wp.float64],
    num_outputs=5,
    in_out_argnames=[
        "neighbor_matrix1",
        "neighbor_matrix_shifts1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)

# Half-fill geometry-only pair-output callables (same I/O as the full-fill ones).
_jax_fill_batch_naive_pair_half_f32 = jax_kernel(
    _fill_batch_naive_pair_half_kernels[wp.float32],
    num_outputs=4,
    in_out_argnames=[
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_batch_naive_pair_half_f64 = jax_kernel(
    _fill_batch_naive_pair_half_kernels[wp.float64],
    num_outputs=4,
    in_out_argnames=[
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_pair_half_f32 = jax_kernel(
    _fill_batch_naive_pbc_pair_half_kernels[wp.float32],
    num_outputs=5,
    in_out_argnames=[
        "neighbor_matrix1",
        "neighbor_matrix_shifts1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_batch_naive_pbc_pair_half_f64 = jax_kernel(
    _fill_batch_naive_pbc_pair_half_kernels[wp.float64],
    num_outputs=5,
    in_out_argnames=[
        "neighbor_matrix1",
        "neighbor_matrix_shifts1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)


@functools.cache
def _get_jax_batch_naive_pair_kernel(
    wp_dtype, pbc_mode: str, half_fill: bool = False, partial: bool = False
):
    """Build a geometry-output batched naive kernel for optional partial rows."""
    kernel = _get_naive_kernel(
        wp_dtype,
        pbc_mode=pbc_mode,
        batched=True,
        selective=False,
        partial=partial,
        return_vectors=True,
        return_distances=True,
        half_fill=half_fill,
    )
    if pbc_mode == "none":
        in_out_argnames = [
            "neighbor_matrix1",
            "num_neighbors1",
            "neighbor_vectors",
            "neighbor_distances",
        ]
    else:
        in_out_argnames = [
            "neighbor_matrix1",
            "neighbor_matrix_shifts1",
            "num_neighbors1",
            "neighbor_vectors",
            "neighbor_distances",
        ]
    return jax_kernel(
        kernel,
        num_outputs=len(in_out_argnames),
        in_out_argnames=in_out_argnames,
        enable_backward=False,
    )


@functools.cache
def _get_jax_batch_naive_pair_fn_kernel(
    pair_fn,
    wp_dtype,
    pbc_mode: str,
    half_fill: bool = False,
    partial: bool = False,
):
    """Build (and cache) a ``jax_kernel`` for a ``pair_fn``-specialized batched naive
    kernel.

    Mirrors ``naive._get_jax_naive_pair_fn_kernel`` with ``batched=True``: the kernel
    is specialized with ``pair_fn`` (so the ``HAS_PAIR_FN`` body runs) and
    ``pair_energies`` / ``pair_forces`` are registered as additional outputs.  Cached
    by ``(pair_fn identity, wp_dtype, pbc_mode)``; one recompile per distinct
    ``pair_fn``.
    """
    kernel = _get_naive_kernel(
        wp_dtype,
        pbc_mode=pbc_mode,
        batched=True,
        selective=False,
        partial=partial,
        return_vectors=True,
        return_distances=True,
        pair_fn=pair_fn,
        half_fill=half_fill,
    )
    if pbc_mode == "none":
        in_out_argnames = [
            "neighbor_matrix1",
            "num_neighbors1",
            "neighbor_vectors",
            "neighbor_distances",
            "pair_energies",
            "pair_forces",
        ]
    else:  # "wrap_on_entry"
        in_out_argnames = [
            "neighbor_matrix1",
            "neighbor_matrix_shifts1",
            "num_neighbors1",
            "neighbor_vectors",
            "neighbor_distances",
            "pair_energies",
            "pair_forces",
        ]
    return jax_kernel(
        kernel,
        num_outputs=len(in_out_argnames),
        in_out_argnames=in_out_argnames,
        enable_backward=False,
    )


# Wrap positions batch kernel wrappers
_jax_wrap_positions_batch_f32 = jax_kernel(
    get_wrap_positions_kernel(wp.float32, batched=True, pbc_aware=True),
    num_outputs=2,
    in_out_argnames=["positions_wrapped", "per_atom_cell_offsets"],
    enable_backward=False,
)
_jax_wrap_positions_batch_f64 = jax_kernel(
    get_wrap_positions_kernel(wp.float64, batched=True, pbc_aware=True),
    num_outputs=2,
    in_out_argnames=["positions_wrapped", "per_atom_cell_offsets"],
    enable_backward=False,
)


def _jax_scalar_sentinels(dtype):
    """Return JAX zero-size placeholders for inactive naive scalar inputs."""
    return (
        jnp.empty((0, 3), dtype=jnp.int32),
        jnp.empty((0, 3, 3), dtype=dtype),
        jnp.empty((0, 3), dtype=jnp.int32),
        jnp.empty((0,), dtype=jnp.int32),
        jnp.empty((0,), dtype=jnp.int32),
        jnp.empty((0,), dtype=jnp.int32),
        jnp.empty((0,), dtype=jnp.int32),
        jnp.empty((0, 0), dtype=jnp.int32),
        jnp.empty((0, 0, 3), dtype=jnp.int32),
        jnp.empty((0,), dtype=jnp.int32),
        jnp.empty((0, 0, 3), dtype=dtype),
        jnp.empty((0, 0), dtype=dtype),
        jnp.empty((0, 0), dtype=dtype),
        jnp.empty((0, 0), dtype=dtype),
        jnp.empty((0, 0, 3), dtype=dtype),
        jnp.empty((0,), dtype=jnp.bool_),
    )


# ==============================================================================
# Tiled-kernel callables (``strategy="tile"``, CUDA-only)
# ==============================================================================
#
# These wrap the *inner* warp launchers ``_launch_naive_neighbor_matrix_no_pbc``
# / ``_launch_naive_neighbor_matrix_pbc`` (with ``batched=True``) inside a
# ``jax_callable`` body and pass ``strategy="tile"`` explicitly, so the
# tile-cooperative ``wp.launch_tiled`` kernel is honored unconditionally
# (unlike the "auto" heuristic, which only tiles for few-large-systems).
#
# Mirrors the single-system tile callables in
# ``nvalchemiops.jax.neighbors.naive`` but for the batched launchers, with two
# batched-specific differences:
#
#   * The launchers square ``cutoff`` internally, so the bodies pass the RAW
#     cutoff (NOT ``cutoff**2``) — unlike the surrounding scalar batched path,
#     which passes ``cutoff*cutoff``.
#   * The wrapped-PBC body passes RAW (unwrapped) ``positions`` with
#     ``wrap_positions=True``; the launcher wraps internally using
#     ``batch_idx`` to pick per-atom cells.  No JAX-side pre-wrap is done on the
#     tile path (that would double-wrap).
#
# Batched PREWRAPPED PBC has no tiled kernel (``_make_tile_kernel`` raises), so
# only no-PBC and wrapped-PBC are wired here; the dispatch site rejects
# ``strategy="tile"`` + ``wrap_positions=False`` for PBC.
#
# These run only on the eager path, where ``batch_naive_neighbor_list`` already
# pre-fills ``neighbor_matrix=fill_value`` and zeroes ``num_neighbors`` /
# shifts before dispatch, so the bodies perform no reset and take no
# ``fill_value`` argument.  Tile supports ``half_fill`` but has no pair-output /
# ``target_indices`` / selective (``rebuild_flags``) variant.  The static
# scalars (``cutoff``, ``half_fill``, ``max_shifts_per_system`` /
# ``max_atoms_per_system`` for PBC) are concrete host ints computed outside jit.


def _batch_naive_tile_no_pbc_f32(
    positions: wp.array(dtype=wp.vec3f),
    batch_idx: wp.array(dtype=wp.int32),
    batch_ptr: wp.array(dtype=wp.int32),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff: wp.float32,
    half_fill: wp.bool,
) -> None:
    _launch_naive_neighbor_matrix_no_pbc(
        positions,
        float(cutoff),
        neighbor_matrix,
        num_neighbors,
        wp.float32,
        str(positions.device),
        batched=True,
        batch_idx=batch_idx,
        batch_ptr=batch_ptr,
        half_fill=bool(half_fill),
        strategy="tile",
    )


def _batch_naive_tile_no_pbc_f64(
    positions: wp.array(dtype=wp.vec3d),
    batch_idx: wp.array(dtype=wp.int32),
    batch_ptr: wp.array(dtype=wp.int32),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff: wp.float64,
    half_fill: wp.bool,
) -> None:
    _launch_naive_neighbor_matrix_no_pbc(
        positions,
        float(cutoff),
        neighbor_matrix,
        num_neighbors,
        wp.float64,
        str(positions.device),
        batched=True,
        batch_idx=batch_idx,
        batch_ptr=batch_ptr,
        half_fill=bool(half_fill),
        strategy="tile",
    )


def _batch_naive_tile_pbc_wrapped_f32(
    positions: wp.array(dtype=wp.vec3f),
    cell: wp.array(dtype=wp.mat33f),
    pbc: wp.array2d(dtype=wp.bool),
    shift_range: wp.array(dtype=wp.vec3i),
    num_shifts_arr: wp.array(dtype=wp.int32),
    batch_idx: wp.array(dtype=wp.int32),
    batch_ptr: wp.array(dtype=wp.int32),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    neighbor_matrix_shifts: wp.array(dtype=wp.vec3i, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff: wp.float32,
    max_shifts_per_system: wp.int32,
    max_atoms_per_system: wp.int32,
    half_fill: wp.bool,
) -> None:
    _launch_naive_neighbor_matrix_pbc(
        positions,
        float(cutoff),
        cell,
        pbc,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        wp.float32,
        str(positions.device),
        batched=True,
        batch_ptr=batch_ptr,
        batch_idx=batch_idx,
        num_shifts_arr=num_shifts_arr,
        max_shifts_per_system=int(max_shifts_per_system),
        max_atoms_per_system=int(max_atoms_per_system),
        half_fill=bool(half_fill),
        wrap_positions=True,
        strategy="tile",
    )


def _batch_naive_tile_pbc_wrapped_f64(
    positions: wp.array(dtype=wp.vec3d),
    cell: wp.array(dtype=wp.mat33d),
    pbc: wp.array2d(dtype=wp.bool),
    shift_range: wp.array(dtype=wp.vec3i),
    num_shifts_arr: wp.array(dtype=wp.int32),
    batch_idx: wp.array(dtype=wp.int32),
    batch_ptr: wp.array(dtype=wp.int32),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    neighbor_matrix_shifts: wp.array(dtype=wp.vec3i, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff: wp.float64,
    max_shifts_per_system: wp.int32,
    max_atoms_per_system: wp.int32,
    half_fill: wp.bool,
) -> None:
    _launch_naive_neighbor_matrix_pbc(
        positions,
        float(cutoff),
        cell,
        pbc,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        wp.float64,
        str(positions.device),
        batched=True,
        batch_ptr=batch_ptr,
        batch_idx=batch_idx,
        num_shifts_arr=num_shifts_arr,
        max_shifts_per_system=int(max_shifts_per_system),
        max_atoms_per_system=int(max_atoms_per_system),
        half_fill=bool(half_fill),
        wrap_positions=True,
        strategy="tile",
    )


# Keyed by ``(has_pbc, wrap_positions)``.  Only no-PBC and wrapped-PBC are
# present; batched prewrapped PBC has no tiled kernel.  Tile has no selective
# variant, so the selective axis is omitted; ``strategy="tile"`` rejects
# ``rebuild_flags`` at the dispatch site.
_BATCH_NAIVE_TILE_NO_PBC_IN_OUT_ARGS = ("neighbor_matrix", "num_neighbors")
_BATCH_NAIVE_TILE_PBC_IN_OUT_ARGS = (
    "neighbor_matrix",
    "neighbor_matrix_shifts",
    "num_neighbors",
)
_BATCH_NAIVE_TILE_SPECS = {
    (False, False): {
        "num_outputs": 2,
        "in_out_argnames": _BATCH_NAIVE_TILE_NO_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _batch_naive_tile_no_pbc_f32,
        jnp.dtype(jnp.float64): _batch_naive_tile_no_pbc_f64,
    },
    (True, True): {
        "num_outputs": 3,
        "in_out_argnames": _BATCH_NAIVE_TILE_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _batch_naive_tile_pbc_wrapped_f32,
        jnp.dtype(jnp.float64): _batch_naive_tile_pbc_wrapped_f64,
    },
}


def _register_batch_naive_tile_callables() -> dict[
    tuple[bool, bool, jnp.dtype], object
]:
    """Register GraphMode.NONE tile callables for the batched naive eager path.

    ``GraphMode.NONE`` (not WARP): the tile bodies assume the caller has
    already pre-filled the output buffers, which the eager
    ``batch_naive_neighbor_list`` path does before dispatch.
    """
    registered: dict[tuple[bool, bool, jnp.dtype], object] = {}
    for (has_pbc, wrap_positions), spec in _BATCH_NAIVE_TILE_SPECS.items():
        for dtype in (jnp.dtype(jnp.float32), jnp.dtype(jnp.float64)):
            registered[(has_pbc, wrap_positions, dtype)] = jax_callable(
                spec[dtype],
                num_outputs=spec["num_outputs"],
                in_out_argnames=spec["in_out_argnames"],
                graph_mode=GraphMode.NONE,
            )
    return registered


_BATCH_NAIVE_TILE_CALLABLES = _register_batch_naive_tile_callables()


def _batch_naive_pair_outputs_forward(
    positions: jax.Array,
    cell: jax.Array | None,
    *,
    pbc: jax.Array | None,
    batch_idx_i32: jax.Array,
    batch_ptr_i32: jax.Array,
    cutoff: float,
    max_neighbors: int,
    fill_value: int,
    max_shifts_per_system: int,
    max_atoms_per_system: int,
    num_systems: int,
    neighbor_matrix: jax.Array | None = None,
    neighbor_matrix_shifts: jax.Array | None = None,
    num_neighbors: jax.Array | None = None,
    neighbor_vectors: jax.Array | None = None,
    neighbor_distances: jax.Array | None = None,
    shift_range_per_dimension: jax.Array | None = None,
    num_shifts_per_system: jax.Array | None = None,
    pair_fn=None,
    pair_params: jax.Array | None = None,
    target_indices: jax.Array | None = None,
    half_fill: bool = False,
) -> _NeighborForwardOutput:
    """Forward closure for the batch_naive autograd path.

    When ``pair_fn`` is set, a ``pair_fn``-specialized kernel writes per-pair
    ``pair_energies`` / ``pair_forces`` which ride along in ``extra_outputs``
    (forward-only); see ``naive._naive_pair_outputs_forward``.
    """
    positions = jax.lax.stop_gradient(positions)
    if cell is not None:
        cell = jax.lax.stop_gradient(cell)
    total_atoms = positions.shape[0]
    is_partial = target_indices is not None
    if is_partial:
        target_indices = jnp.asarray(target_indices, dtype=jnp.int32)
        num_rows = int(target_indices.shape[0])
    else:
        num_rows = total_atoms
    f64 = positions.dtype == jnp.float64
    wp_dtype = wp.float64 if f64 else wp.float32
    cutoff_sq = float(cutoff * cutoff)
    (
        empty_offsets,
        empty_cell,
        empty_shift_range,
        empty_num_shifts,
        empty_batch_idx,
        empty_batch_ptr,
        empty_target_indices,
        empty_matrix,
        empty_shifts,
        empty_num_neighbors,
        empty_vectors,
        empty_distances,
        empty_pair_params,
        empty_energies,
        empty_forces,
        empty_rebuild_flags,
    ) = _jax_scalar_sentinels(positions.dtype)

    ti_arg = target_indices if is_partial else empty_target_indices
    if neighbor_matrix is None:
        nm = jnp.full((num_rows, max_neighbors), fill_value, dtype=jnp.int32)
    else:
        nm = neighbor_matrix.at[:].set(jnp.int32(fill_value))
    if num_neighbors is None:
        nn = jnp.zeros(num_rows, dtype=jnp.int32)
    else:
        nn = num_neighbors.at[:].set(jnp.int32(0))
    if neighbor_matrix_shifts is None:
        nms = jnp.zeros((num_rows, max_neighbors, 3), dtype=jnp.int32)
    else:
        nms = neighbor_matrix_shifts.at[:].set(jnp.int32(0))
    if neighbor_vectors is None:
        nv = jnp.zeros((num_rows, max_neighbors, 3), dtype=positions.dtype)
    else:
        nv = neighbor_vectors.at[:].set(jnp.asarray(0.0, dtype=positions.dtype))
    if neighbor_distances is None:
        nd = jnp.zeros((num_rows, max_neighbors), dtype=positions.dtype)
    else:
        nd = neighbor_distances.at[:].set(jnp.asarray(0.0, dtype=positions.dtype))

    # ``pair_fn`` path: real per-atom params + auto-allocated energy/force buffers
    # (returned via ``extra_outputs``, forward-only).
    has_pair_fn = pair_fn is not None
    if has_pair_fn:
        pp_arg = jnp.asarray(pair_params, dtype=positions.dtype)
        pe = jnp.zeros((num_rows, max_neighbors), dtype=positions.dtype)
        pf = jnp.zeros((num_rows, max_neighbors, 3), dtype=positions.dtype)
    else:
        pp_arg = empty_pair_params
        pe = None
        pf = None

    if pbc is None:
        if has_pair_fn:
            kernel = _get_jax_batch_naive_pair_fn_kernel(
                pair_fn, wp_dtype, "none", half_fill, is_partial
            )
        elif is_partial:
            kernel = _get_jax_batch_naive_pair_kernel(
                wp_dtype, "none", half_fill, is_partial
            )
        elif half_fill:
            kernel = (
                _jax_fill_batch_naive_pair_half_f64
                if f64
                else _jax_fill_batch_naive_pair_half_f32
            )
        else:
            kernel = (
                _jax_fill_batch_naive_pair_f64
                if f64
                else _jax_fill_batch_naive_pair_f32
            )
        outs = kernel(
            positions,
            empty_offsets,
            cutoff_sq,
            0.0,
            empty_cell,
            empty_shift_range,
            empty_num_shifts,
            batch_idx_i32,
            batch_ptr_i32,
            ti_arg,
            nm,
            empty_shifts,
            nn,
            empty_matrix,
            empty_shifts,
            empty_num_neighbors,
            nv,
            nd,
            pp_arg,
            pe if has_pair_fn else empty_energies,
            pf if has_pair_fn else empty_forces,
            empty_rebuild_flags,
            launch_dims=(1, 1, num_rows),
        )
        if has_pair_fn:
            nm, nn, nv, nd, pe, pf = outs
        else:
            nm, nn, nv, nd = outs
    else:
        if has_pair_fn:
            kernel = _get_jax_batch_naive_pair_fn_kernel(
                pair_fn, wp_dtype, "wrap_on_entry", half_fill, is_partial
            )
        elif is_partial:
            kernel = _get_jax_batch_naive_pair_kernel(
                wp_dtype, "wrap_on_entry", half_fill, is_partial
            )
        elif half_fill:
            kernel = (
                _jax_fill_batch_naive_pbc_pair_half_f64
                if f64
                else _jax_fill_batch_naive_pbc_pair_half_f32
            )
        else:
            kernel = (
                _jax_fill_batch_naive_pbc_pair_f64
                if f64
                else _jax_fill_batch_naive_pbc_pair_f32
            )
        if shift_range_per_dimension is None or num_shifts_per_system is None:
            shift_range_per_dimension, num_shifts_per_system, _ = (
                compute_naive_num_shifts(cell, cutoff, pbc)
            )
        inv_cell = jnp.linalg.inv(cell)
        positions_wrapped = jnp.zeros_like(positions)
        per_atom_cell_offsets = jnp.zeros((total_atoms, 3), dtype=jnp.int32)
        if f64:
            _wrap_kernel = _jax_wrap_positions_batch_f64
        else:
            _wrap_kernel = _jax_wrap_positions_batch_f32
        positions_wrapped, per_atom_cell_offsets = _wrap_kernel(
            positions,
            cell,
            inv_cell,
            pbc,
            batch_idx_i32,
            positions_wrapped,
            per_atom_cell_offsets,
            launch_dims=(total_atoms,),
        )
        outs = kernel(
            positions_wrapped,
            per_atom_cell_offsets,
            cutoff_sq,
            0.0,
            cell,
            shift_range_per_dimension,
            num_shifts_per_system,
            batch_idx_i32,
            batch_ptr_i32,
            ti_arg,
            nm,
            nms,
            nn,
            empty_matrix,
            empty_shifts,
            empty_num_neighbors,
            nv,
            nd,
            pp_arg,
            pe if has_pair_fn else empty_energies,
            pf if has_pair_fn else empty_forces,
            empty_rebuild_flags,
            launch_dims=(
                1 if is_partial else num_systems,
                (2 * max_shifts_per_system - 1)
                if is_partial and not half_fill
                else max_shifts_per_system,
                num_rows if is_partial else max_atoms_per_system,
            ),
        )
        if has_pair_fn:
            nm, nms, nn, nv, nd, pe, pf = outs
        else:
            nm, nms, nn, nv, nd = outs

    i_idx, j_idx, shifts_ret, _, mask_ = _build_index_residuals(
        nm,
        nn,
        nms,
        target_indices=target_indices if is_partial else None,
    )
    K, M = nm.shape
    extra_outputs = (nm, nn, nms, pe, pf) if has_pair_fn else (nm, nn, nms)
    return _NeighborForwardOutput(
        distances=nd,
        vectors=nv,
        extra_outputs=extra_outputs,
        i_idx=i_idx,
        j_idx=j_idx,
        shifts=shifts_ret,
        batch_idx=batch_idx_i32,
        active_mask=mask_,
        matrix_shape=(K, M),
    )


def _validate_pair_output_buffer(
    name: str,
    array: jax.Array | None,
    expected_shape: tuple[int, ...],
    expected_dtype=None,
) -> None:
    """Validate optional matrix-shaped output buffers for pair-output paths."""
    if array is None:
        return
    if tuple(array.shape) != expected_shape:
        raise ValueError(
            f"{name} must have shape {expected_shape}; got {tuple(array.shape)}.",
        )
    if expected_dtype is not None and array.dtype != expected_dtype:
        raise ValueError(
            f"{name} dtype must be {expected_dtype}; got {array.dtype}.",
        )


[docs] def batch_naive_neighbor_list( positions: jax.Array, cutoff: float, batch_idx: jax.Array | None = None, batch_ptr: jax.Array | None = None, pbc: jax.Array | None = None, cell: jax.Array | None = None, max_neighbors: int | None = None, half_fill: bool = False, fill_value: int | None = None, return_neighbor_list: bool = False, neighbor_matrix: jax.Array | None = None, neighbor_matrix_shifts: jax.Array | None = None, num_neighbors: jax.Array | None = None, shift_range_per_dimension: jax.Array | None = None, num_shifts_per_system: jax.Array | None = None, max_shifts_per_system: int | None = None, max_atoms_per_system: int | None = None, rebuild_flags: jax.Array | None = None, wrap_positions: bool = True, positions_wrapped_buffer: jax.Array | None = None, per_atom_cell_offsets_buffer: jax.Array | None = None, inv_cell_buffer: jax.Array | None = None, strategy: str = "auto", *, return_distances: bool = False, return_vectors: bool = False, neighbor_vectors: jax.Array | None = None, neighbor_distances: jax.Array | None = None, target_indices: jax.Array | None = None, pair_fn=None, pair_params: jax.Array | None = None, pair_energies: jax.Array | None = None, pair_forces: jax.Array | None = None, ) -> tuple[jax.Array, ...]: """Compute neighbor list for batch of systems using naive O(N^2) algorithm. Identifies all atom pairs within a specified cutoff distance for each system independently using a brute-force pairwise distance calculation. Supports both non-periodic and periodic boundary conditions. Parameters ---------- positions : jax.Array, shape (total_atoms, 3), dtype=float32 or float64 Concatenated Cartesian coordinates for all systems. cutoff : float Cutoff distance for neighbor detection in Cartesian units. Must be positive. Atoms within this distance are considered neighbors. batch_idx : jax.Array, shape (total_atoms,), dtype=int32, optional System index for each atom. If None, batch_ptr must be provided. batch_ptr : jax.Array, shape (num_systems + 1,), dtype=int32, optional Cumulative atom counts defining system boundaries. If None, batch_idx must be provided. pbc : jax.Array, shape (num_systems, 3), dtype=bool, optional Periodic boundary condition flags for each system and dimension. True enables periodicity in that direction. Default is None (no PBC). cell : jax.Array, shape (num_systems, 3, 3), dtype=float32 or float64, optional Cell matrices defining lattice vectors. Required if pbc is provided. max_neighbors : int, optional Maximum number of neighbors per atom. half_fill : bool, optional If True, only store relationships where i < j. Default is False. fill_value : int, optional Value to fill the neighbor matrix with. Default is total_atoms. neighbor_matrix : jax.Array, shape (num_rows, max_neighbors), optional Pre-shaped neighbor matrix. ``num_rows`` is ``total_atoms`` normally and ``len(target_indices)`` for partial rows. neighbor_matrix_shifts : jax.Array, shape (num_rows, max_neighbors, 3), optional Pre-shaped shift matrix for PBC. num_neighbors : jax.Array, shape (num_rows,), optional Pre-shaped neighbors count array. shift_range_per_dimension : jax.Array, optional Pre-computed shift range for PBC systems. num_shifts_per_system : jax.Array, optional Number of periodic shifts per system. max_shifts_per_system : int, optional Maximum per-system shift count (launch dimension). max_atoms_per_system : int, optional Maximum atoms in any system. wrap_positions : bool, default=True If True, wrap input positions into the primary cell before neighbor search. Set to False when positions are already wrapped (e.g. by a preceding integration step) to save two GPU kernel launches per call. strategy : {"auto", "scalar", "tile"}, default="auto" Selects the underlying Warp kernel variant. ``"scalar"`` uses the per-atom scalar kernel. ``"tile"`` uses the tile-cooperative ``wp.launch_tiled`` kernel and is **CUDA-only**: requesting it on a CPU device raises ``ValueError``. The tile path supports the no-PBC and PBC-wrapped (``wrap_positions=True``) cases and ``half_fill``, but has no pair-output (``return_distances`` / ``return_vectors``) or selective (``rebuild_flags``) variant, and there is **no batched prewrapped-PBC tiled kernel**: requesting ``strategy="tile"`` with PBC and ``wrap_positions=False`` raises ``NotImplementedError`` (use ``"scalar"`` for that combination). ``"auto"`` and ``"scalar"`` preserve the current scalar-dispatch behavior; ``"auto"`` never selects tile in this binding (tile is opt-in). The tile and scalar paths produce identical pair *sets* (per-row ordering may differ; under ``half_fill`` the two pick opposite pair owners, yielding the same undirected set with sign-flipped shifts). neighbor_distances : jax.Array, shape (num_rows, max_neighbors), optional Pre-shaped distance output for ``return_distances=True`` or ``pair_fn``. neighbor_vectors : jax.Array, shape (num_rows, max_neighbors, 3), optional Pre-shaped vector output for ``return_vectors=True`` or ``pair_fn``. target_indices : jax.Array, shape (num_targets,), dtype=int32, optional Compact partial-list source rows. Output row ``r`` maps to atom ``target_indices[r]``; COO source rows remain compact row ids. User buffers must be compact-row shaped, not full atom-row shaped. Returns ------- results : tuple of jax.Array Variable-length tuple depending on input parameters. Matrix outputs use ``num_rows`` rows, where ``num_rows`` is ``total_atoms`` normally and ``len(target_indices)`` for partial lists. COO pointer arrays have shape ``(num_rows + 1,)`` and source ids are compact rows when ``target_indices`` is provided. Examples -------- Basic usage with batch_ptr: >>> import jax.numpy as jnp >>> from nvalchemiops.jax.neighbors import batch_naive_neighbor_list >>> positions = jnp.zeros((200, 3), dtype=jnp.float32) >>> batch_ptr = jnp.array([0, 100, 200], dtype=jnp.int32) # 2 systems >>> cutoff = 2.5 >>> max_neighbors = 50 >>> neighbor_matrix, num_neighbors = batch_naive_neighbor_list( ... positions, cutoff, batch_ptr=batch_ptr, max_neighbors=max_neighbors ... ) With PBC: >>> cell = jnp.eye(3, dtype=jnp.float32)[jnp.newaxis, :, :] * 10.0 >>> cell = jnp.repeat(cell, 2, axis=0) >>> pbc = jnp.ones((2, 3), dtype=jnp.bool_) >>> neighbor_matrix, num_neighbors, shifts = batch_naive_neighbor_list( ... positions, cutoff, batch_ptr=batch_ptr, max_neighbors=max_neighbors, ... pbc=pbc, cell=cell ... ) See Also -------- nvalchemiops.neighbors.batch_naive.batch_naive_neighbor_matrix : Core warp launcher nvalchemiops.jax.neighbors.naive.naive_neighbor_list : Non-batched version batch_cell_list : Cell list method for large systems """ if strategy not in {"auto", "scalar", "tile"}: raise ValueError( f"strategy must be 'auto' | 'scalar' | 'tile', got {strategy!r}", ) if pbc is None and cell is not None: raise ValueError("If cell is provided, pbc must also be provided") if pbc is not None and cell is None: raise ValueError("If pbc is provided, cell must also be provided") if strategy == "tile": # The tile-cooperative kernel is CUDA-only and has no pair-output or # selective (rebuild_flags) variant, and no batched prewrapped-PBC # tiled kernel. Gate here, before any launch, mirroring the warp # launcher CPU guard and the single-system tile guards. if _is_jax_cpu_array(positions): raise ValueError( "strategy='tile' requires CUDA; the tile-cooperative " "naive kernel cannot run on a CPU device (Warp forces " "block_dim=1). Use strategy='scalar' or 'auto' on CPU.", ) if bool(return_distances) or bool(return_vectors) or pair_fn is not None: raise NotImplementedError( "strategy='tile' has no pair-output (return_distances / " "return_vectors / pair_fn) variant; use strategy='scalar'.", ) if target_indices is not None: raise NotImplementedError( "strategy='tile' has no target_indices (partial " "neighbor-list) variant; use strategy='scalar'.", ) if rebuild_flags is not None: raise NotImplementedError( "strategy='tile' has no selective (rebuild_flags) " "variant; use strategy='scalar'.", ) if pbc is not None and not wrap_positions: raise NotImplementedError( "strategy='tile' has no batched prewrapped-PBC tiled " "kernel (wrap_positions=False with PBC). Use " "strategy='scalar', or wrap_positions=True for the " "tile path.", ) # Prepare batch indices and pointers batch_idx, batch_ptr = prepare_batch_idx_ptr( batch_idx, batch_ptr, positions.shape[0] ) num_systems = batch_ptr.shape[0] - 1 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).", ) if pair_fn is None: if pair_params is not None: raise ValueError("pair_params requires pair_fn.") if pair_energies is not None: raise ValueError("pair_energies requires pair_fn.") if pair_forces is not None: raise ValueError("pair_forces requires pair_fn.") has_pair_outputs = ( bool(return_distances) or bool(return_vectors) or pair_fn is not None or target_indices is not None ) if has_pair_outputs: if rebuild_flags is not None: raise NotImplementedError( "Pair outputs are not supported with rebuild_flags.", ) num_rows = ( int(target_indices.shape[0]) if target_indices is not None else int(positions.shape[0]) ) if max_neighbors is None and neighbor_matrix is not None: max_neighbors = int(neighbor_matrix.shape[1]) if max_neighbors is None: max_neighbors = estimate_max_neighbors(cutoff) if fill_value is None: fill_value = positions.shape[0] _validate_pair_output_buffer( "neighbor_matrix", neighbor_matrix, (num_rows, int(max_neighbors)), jnp.int32, ) _validate_pair_output_buffer( "num_neighbors", num_neighbors, (num_rows,), jnp.int32, ) if pbc is not None: _validate_pair_output_buffer( "neighbor_matrix_shifts", neighbor_matrix_shifts, (num_rows, int(max_neighbors), 3), jnp.int32, ) _validate_pair_output_buffer( "neighbor_distances", neighbor_distances, (num_rows, int(max_neighbors)), positions.dtype, ) _validate_pair_output_buffer( "neighbor_vectors", neighbor_vectors, (num_rows, int(max_neighbors), 3), positions.dtype, ) cell_norm = cell if cell_norm is not None: cell_norm = ( cell_norm if cell_norm.ndim == 3 else cell_norm[jnp.newaxis, :, :] ) if cell_norm.dtype != positions.dtype: cell_norm = cell_norm.astype(positions.dtype) pbc_norm = pbc if pbc_norm is not None: pbc_norm = pbc_norm if pbc_norm.ndim == 2 else pbc_norm[jnp.newaxis, :] batch_idx_i32 = batch_idx.astype(jnp.int32) batch_ptr_i32 = batch_ptr.astype(jnp.int32) if pbc_norm is not None: if ( shift_range_per_dimension is None or num_shifts_per_system is None or max_shifts_per_system is None ): ( shift_range_per_dimension, num_shifts_per_system, max_shifts_per_system, ) = compute_naive_num_shifts( jax.lax.stop_gradient(cell_norm), cutoff, pbc_norm, ) try: max_shifts_per_system = int(max_shifts_per_system) except ( jax.errors.ConcretizationTypeError, jax.errors.TracerIntegerConversionError, ) as exc: raise ValueError( "max_shifts_per_system must be passed as a concrete int when " "calling batch_naive_neighbor_list under jax.jit with PBC " "and target_indices / pair outputs.", ) from exc if max_atoms_per_system is None: try: max_atoms_per_system = int(jnp.max(batch_ptr[1:] - batch_ptr[:-1])) except ( jax.errors.ConcretizationTypeError, jax.errors.TracerIntegerConversionError, ): raise ValueError( "max_atoms_per_system must be passed explicitly when " "calling batch_naive_neighbor_list under jax.jit with " "return_distances / return_vectors set. The autograd " "path needs a concrete launch dimension and cannot " "infer it from a traced batch_ptr." ) from None else: max_shifts_per_system = 1 max_atoms_per_system = positions.shape[0] forward_kwargs = { "pbc": pbc_norm, "batch_idx_i32": batch_idx_i32, "batch_ptr_i32": batch_ptr_i32, "cutoff": float(cutoff), "max_neighbors": int(max_neighbors), "fill_value": int(fill_value), "max_shifts_per_system": int(max_shifts_per_system), "max_atoms_per_system": int(max_atoms_per_system), "num_systems": int(num_systems), "neighbor_matrix": neighbor_matrix, "neighbor_matrix_shifts": neighbor_matrix_shifts, "num_neighbors": num_neighbors, "neighbor_vectors": neighbor_vectors, "neighbor_distances": neighbor_distances, "shift_range_per_dimension": shift_range_per_dimension, "num_shifts_per_system": num_shifts_per_system, "pair_fn": pair_fn, "pair_params": pair_params, "target_indices": target_indices, "half_fill": bool(half_fill), } route_out = _route_pair_outputs( positions, cell_norm, _batch_naive_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 return_neighbor_list: if pbc is not None: 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) else: nl, nptr = get_neighbor_list_from_neighbor_matrix( nm_out, num_neighbors=nn_out, fill_value=int(fill_value), ) base = (nl, nptr) # Repack per-pair geometry (and pair_fn outputs) into the same COO order # as ``nl``. Eager-only, like the index conversion. 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) elif pbc is not None: base = (nm_out, nn_out, shifts_out) else: base = (nm_out, nn_out) # Return tail mirrors the torch contract: optional distances / vectors, then # (pe, pf) whenever ``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) if cell is not None: cell = cell if cell.ndim == 3 else cell[jnp.newaxis, :, :] # Ensure cell dtype matches positions dtype so Warp kernel dispatch is consistent if cell.dtype != positions.dtype: cell = cell.astype(positions.dtype) if pbc is not None: pbc = pbc if pbc.ndim == 2 else pbc[jnp.newaxis, :] if max_neighbors is None: max_neighbors = estimate_max_neighbors(cutoff) if fill_value is None: fill_value = jnp.int32(positions.shape[0]) if neighbor_matrix is None: neighbor_matrix = jnp.full( (positions.shape[0], max_neighbors), fill_value, dtype=jnp.int32, ) elif rebuild_flags is None: neighbor_matrix = neighbor_matrix.at[:].set(fill_value) if num_neighbors is None: num_neighbors = jnp.zeros(positions.shape[0], dtype=jnp.int32) elif rebuild_flags is None: num_neighbors = num_neighbors.at[:].set(jnp.int32(0)) if pbc is not None: if neighbor_matrix_shifts is None: neighbor_matrix_shifts = jnp.zeros( (positions.shape[0], max_neighbors, 3), dtype=jnp.int32, ) elif rebuild_flags is None: neighbor_matrix_shifts = neighbor_matrix_shifts.at[:].set(jnp.int32(0)) if ( max_shifts_per_system is None or num_shifts_per_system is None or shift_range_per_dimension is None ): shift_range_per_dimension, num_shifts_per_system, max_shifts_per_system = ( compute_naive_num_shifts(cell, cutoff, pbc) ) if cutoff <= 0: if return_neighbor_list: if pbc is not None: return ( jnp.zeros((2, 0), dtype=jnp.int32), jnp.zeros((positions.shape[0] + 1,), dtype=jnp.int32), jnp.zeros((0, 3), dtype=jnp.int32), ) else: return ( jnp.zeros((2, 0), dtype=jnp.int32), jnp.zeros((positions.shape[0] + 1,), dtype=jnp.int32), ) else: if pbc is not None: return neighbor_matrix, num_neighbors, neighbor_matrix_shifts else: return neighbor_matrix, num_neighbors # Select kernel based on dtype and static half-fill specialization. if positions.dtype == jnp.float64: if half_fill: _jax_fill = _jax_fill_batch_naive_half_f64 _jax_fill_pbc = _jax_fill_batch_naive_pbc_half_f64 _jax_fill_selective = _jax_fill_batch_naive_selective_half_f64 _jax_fill_pbc_selective = _jax_fill_batch_naive_pbc_selective_half_f64 _jax_fill_pbc_prewrapped = _jax_fill_batch_naive_pbc_prewrapped_half_f64 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_batch_naive_pbc_prewrapped_selective_half_f64 ) else: _jax_fill = _jax_fill_batch_naive_f64 _jax_fill_pbc = _jax_fill_batch_naive_pbc_f64 _jax_fill_selective = _jax_fill_batch_naive_selective_f64 _jax_fill_pbc_selective = _jax_fill_batch_naive_pbc_selective_f64 _jax_fill_pbc_prewrapped = _jax_fill_batch_naive_pbc_prewrapped_f64 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_batch_naive_pbc_prewrapped_selective_f64 ) _jax_wrap_batch = _jax_wrap_positions_batch_f64 else: if half_fill: _jax_fill = _jax_fill_batch_naive_half_f32 _jax_fill_pbc = _jax_fill_batch_naive_pbc_half_f32 _jax_fill_selective = _jax_fill_batch_naive_selective_half_f32 _jax_fill_pbc_selective = _jax_fill_batch_naive_pbc_selective_half_f32 _jax_fill_pbc_prewrapped = _jax_fill_batch_naive_pbc_prewrapped_half_f32 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_batch_naive_pbc_prewrapped_selective_half_f32 ) else: _jax_fill = _jax_fill_batch_naive_f32 _jax_fill_pbc = _jax_fill_batch_naive_pbc_f32 _jax_fill_selective = _jax_fill_batch_naive_selective_f32 _jax_fill_pbc_selective = _jax_fill_batch_naive_pbc_selective_f32 _jax_fill_pbc_prewrapped = _jax_fill_batch_naive_pbc_prewrapped_f32 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_batch_naive_pbc_prewrapped_selective_f32 ) _jax_wrap_batch = _jax_wrap_positions_batch_f32 positions = positions.astype(jnp.float32) positions = jax.lax.stop_gradient(positions) if cell is not None: cell = jax.lax.stop_gradient(cell) if inv_cell_buffer is not None: inv_cell_buffer = jax.lax.stop_gradient(inv_cell_buffer) if positions_wrapped_buffer is not None: positions_wrapped_buffer = jax.lax.stop_gradient(positions_wrapped_buffer) if per_atom_cell_offsets_buffer is not None: per_atom_cell_offsets_buffer = jax.lax.stop_gradient( per_atom_cell_offsets_buffer ) total_atoms = positions.shape[0] batch_idx_i32 = batch_idx.astype(jnp.int32) batch_ptr_i32 = batch_ptr.astype(jnp.int32) ( empty_offsets, empty_cell, empty_shift_range, empty_num_shifts, empty_batch_idx, empty_batch_ptr, empty_target_indices, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, empty_rebuild_flags, ) = _jax_scalar_sentinels(positions.dtype) if strategy == "tile": # CUDA-only tile-cooperative path (eager only). Output buffers were # already pre-filled above; the callable bodies wrap the batched inner # warp launchers with strategy="tile". The launchers square the # cutoff internally, so the RAW cutoff is threaded as a static scalar # (NOT cutoff*cutoff, unlike the scalar arms below). cutoff_static = float(cutoff) if pbc is None: tile_callable = _BATCH_NAIVE_TILE_CALLABLES[(False, False, positions.dtype)] neighbor_matrix, num_neighbors = tile_callable( positions, batch_idx_i32, batch_ptr_i32, neighbor_matrix, num_neighbors, cutoff_static, half_fill, ) else: # Wrapped-PBC tile (prewrapped already rejected at the guard). The # launcher wraps RAW positions internally using batch_idx, so no # JAX-side pre-wrap is done here (that would double-wrap). if cell.dtype != positions.dtype: cell = cell.astype(positions.dtype) if max_atoms_per_system is None: try: max_atoms_per_system = int(jnp.max(batch_ptr[1:] - batch_ptr[:-1])) except ( jax.errors.ConcretizationTypeError, jax.errors.TracerIntegerConversionError, ): raise ValueError( "Cannot infer max_atoms_per_system inside jax.jit. " "Please provide max_atoms_per_system explicitly when " "using jax.jit with strategy='tile'." ) from None tile_callable = _BATCH_NAIVE_TILE_CALLABLES[(True, True, positions.dtype)] neighbor_matrix, neighbor_matrix_shifts, num_neighbors = tile_callable( positions, cell, pbc, shift_range_per_dimension, num_shifts_per_system, batch_idx_i32, batch_ptr_i32, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, cutoff_static, int(max_shifts_per_system), int(max_atoms_per_system), half_fill, ) elif pbc is None: # No PBC case if rebuild_flags is not None: rf = rebuild_flags.astype(jnp.bool_) atom_rebuild = rf[batch_idx_i32] num_neighbors = jnp.where( atom_rebuild, jnp.zeros_like(num_neighbors), num_neighbors ) neighbor_matrix, num_neighbors = _jax_fill_selective( positions, empty_offsets, float(cutoff * cutoff), 0.0, empty_cell, empty_shift_range, empty_num_shifts, batch_idx_i32, batch_ptr_i32, empty_target_indices, neighbor_matrix, empty_shifts, num_neighbors, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, rf, launch_dims=(1, 1, total_atoms), ) else: neighbor_matrix, num_neighbors = _jax_fill( positions, empty_offsets, float(cutoff * cutoff), 0.0, empty_cell, empty_shift_range, empty_num_shifts, batch_idx_i32, batch_ptr_i32, empty_target_indices, neighbor_matrix, empty_shifts, num_neighbors, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, empty_rebuild_flags, launch_dims=(1, 1, total_atoms), ) else: if cell.dtype != positions.dtype: cell = cell.astype(positions.dtype) if max_atoms_per_system is None: try: max_atoms_per_system = int(jnp.max(batch_ptr[1:] - batch_ptr[:-1])) except ( jax.errors.ConcretizationTypeError, jax.errors.TracerIntegerConversionError, ): raise ValueError( "Cannot infer max_atoms_per_system inside jax.jit. " "Please provide max_atoms_per_system explicitly when using jax.jit." ) from None if wrap_positions: inv_cell = ( inv_cell_buffer if inv_cell_buffer is not None else jnp.linalg.inv(cell) ) positions_wrapped = ( positions_wrapped_buffer if positions_wrapped_buffer is not None else jnp.zeros_like(positions) ) per_atom_cell_offsets = ( per_atom_cell_offsets_buffer if per_atom_cell_offsets_buffer is not None else jnp.zeros((total_atoms, 3), dtype=jnp.int32) ) positions_wrapped, per_atom_cell_offsets = _jax_wrap_batch( positions, cell, inv_cell, pbc, batch_idx_i32, positions_wrapped, per_atom_cell_offsets, launch_dims=(total_atoms,), ) if rebuild_flags is not None: rf = rebuild_flags.astype(jnp.bool_) atom_rebuild = rf[batch_idx_i32] num_neighbors = jnp.where( atom_rebuild, jnp.zeros_like(num_neighbors), num_neighbors ) neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( _jax_fill_pbc_selective( positions_wrapped, per_atom_cell_offsets, float(cutoff * cutoff), 0.0, cell, shift_range_per_dimension, num_shifts_per_system, batch_idx_i32, batch_ptr_i32, empty_target_indices, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, rf, launch_dims=( num_systems, max_shifts_per_system, max_atoms_per_system, ), ) ) else: neighbor_matrix, neighbor_matrix_shifts, num_neighbors = _jax_fill_pbc( positions_wrapped, per_atom_cell_offsets, float(cutoff * cutoff), 0.0, cell, shift_range_per_dimension, num_shifts_per_system, batch_idx_i32, batch_ptr_i32, empty_target_indices, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, empty_rebuild_flags, launch_dims=( num_systems, max_shifts_per_system, max_atoms_per_system, ), ) else: if rebuild_flags is not None: rf = rebuild_flags.astype(jnp.bool_) atom_rebuild = rf[batch_idx_i32] num_neighbors = jnp.where( atom_rebuild, jnp.zeros_like(num_neighbors), num_neighbors ) neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( _jax_fill_pbc_prewrapped_selective( positions, empty_offsets, float(cutoff * cutoff), 0.0, cell, shift_range_per_dimension, num_shifts_per_system, batch_idx_i32, batch_ptr_i32, empty_target_indices, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, rf, launch_dims=( num_systems, max_shifts_per_system, max_atoms_per_system, ), ) ) else: neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( _jax_fill_pbc_prewrapped( positions, empty_offsets, float(cutoff * cutoff), 0.0, cell, shift_range_per_dimension, num_shifts_per_system, batch_idx_i32, batch_ptr_i32, empty_target_indices, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, empty_matrix, empty_shifts, empty_num_neighbors, empty_vectors, empty_distances, empty_pair_params, empty_energies, empty_forces, empty_rebuild_flags, launch_dims=( num_systems, max_shifts_per_system, max_atoms_per_system, ), ) ) if return_neighbor_list: if pbc is not None: neighbor_list, neighbor_ptr, neighbor_list_shifts = ( get_neighbor_list_from_neighbor_matrix( neighbor_matrix, num_neighbors=num_neighbors, neighbor_shift_matrix=neighbor_matrix_shifts, fill_value=fill_value, ) ) return neighbor_list, neighbor_ptr, neighbor_list_shifts else: neighbor_list, neighbor_ptr = get_neighbor_list_from_neighbor_matrix( neighbor_matrix, num_neighbors=num_neighbors, fill_value=fill_value, ) return neighbor_list, neighbor_ptr else: if pbc is not None: return neighbor_matrix, num_neighbors, neighbor_matrix_shifts else: return neighbor_matrix, num_neighbors