Source code for nvalchemiops.jax.neighbors.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 unbatched naive O(N^2) neighbor list construction."""

from __future__ import annotations

import functools
from typing import Literal

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 (
    _validate_graph_mode,
    build_naive_kernel_tables,
    compute_naive_num_shifts,
    coo_pack_pair_geometry,
    get_neighbor_list_from_neighbor_matrix,
)
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 (
    DTYPE_INFO_ALL,
    empty_sentinel,
    estimate_max_neighbors,
    get_wrap_positions_kernel,
    resolve_buffer_alias,
    selective_zero_num_neighbors_single,
)

_DTYPE_TO_NAIVE_KERNELS = (wp.float32, wp.float64)
(
    _fill_naive_neighbor_matrix_kernels,
    _fill_naive_neighbor_matrix_selective_kernels,
    _fill_naive_neighbor_matrix_pbc_kernels,
    _fill_naive_neighbor_matrix_pbc_selective_kernels,
    _fill_naive_neighbor_matrix_pbc_prewrapped_kernels,
    _fill_naive_neighbor_matrix_pbc_prewrapped_selective_kernels,
) = build_naive_kernel_tables(
    "single_cutoff", batched=False, dtypes=_DTYPE_TO_NAIVE_KERNELS
)

(
    _fill_naive_neighbor_matrix_half_kernels,
    _fill_naive_neighbor_matrix_selective_half_kernels,
    _fill_naive_neighbor_matrix_pbc_half_kernels,
    _fill_naive_neighbor_matrix_pbc_selective_half_kernels,
    _fill_naive_neighbor_matrix_pbc_prewrapped_half_kernels,
    _fill_naive_neighbor_matrix_pbc_prewrapped_selective_half_kernels,
) = build_naive_kernel_tables(
    "single_cutoff",
    batched=False,
    dtypes=_DTYPE_TO_NAIVE_KERNELS,
    half_fill=True,
)

# Pair-output variants — produced by the same factory but with
# ``return_vectors`` / ``return_distances`` flipped on.  Used by the
# autograd path in :mod:`nvalchemiops.jax.neighbors._autograd`.
#
# The PBC variant is hard-wired to ``pbc_mode='wrap_on_entry'``.  The kernel
# is idempotent on already-wrapped positions and produces correct shifts
# for raw (unwrapped) positions as well, so the autograd path silently
# ignores the public ``wrap_positions`` kwarg.  Callers who pre-wrap to save
# the two extra kernel launches lose that optimization on the autograd
# path but retain numerical equivalence.

_fill_naive_pair_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="none",
        batched=False,
        selective=False,
        return_vectors=True,
        return_distances=True,
    )
    for t in _DTYPE_TO_NAIVE_KERNELS
}
_fill_naive_pbc_pair_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="wrap_on_entry",
        batched=False,
        selective=False,
        return_vectors=True,
        return_distances=True,
    )
    for t in _DTYPE_TO_NAIVE_KERNELS
}

# Half-fill specializations of the pair-output kernels (``half_fill`` is a
# compile-time constant in the Warp factory, so honoring it needs a distinct
# kernel).  Selected by the forward when ``half_fill=True``.
_fill_naive_pair_half_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="none",
        batched=False,
        selective=False,
        return_vectors=True,
        return_distances=True,
        half_fill=True,
    )
    for t in _DTYPE_TO_NAIVE_KERNELS
}
_fill_naive_pbc_pair_half_kernels = {
    t: _get_naive_kernel(
        t,
        pbc_mode="wrap_on_entry",
        batched=False,
        selective=False,
        return_vectors=True,
        return_distances=True,
        half_fill=True,
    )
    for t in _DTYPE_TO_NAIVE_KERNELS
}

__all__ = ["naive_neighbor_list"]

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

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

# PBC naive neighbor matrix kernel wrappers
_jax_fill_naive_pbc_f32 = jax_kernel(
    _fill_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_naive_pbc_f64 = jax_kernel(
    _fill_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 naive neighbor matrix kernel wrappers
_jax_fill_naive_selective_f32 = jax_kernel(
    _fill_naive_neighbor_matrix_selective_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_naive_selective_f64 = jax_kernel(
    _fill_naive_neighbor_matrix_selective_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)

# Selective PBC naive neighbor matrix kernel wrappers
_jax_fill_naive_pbc_selective_f32 = jax_kernel(
    _fill_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_naive_pbc_selective_f64 = jax_kernel(
    _fill_naive_neighbor_matrix_pbc_selective_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)

# PBC prewrapped naive neighbor matrix kernel wrappers
_jax_fill_naive_pbc_prewrapped_f32 = jax_kernel(
    _fill_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_naive_pbc_prewrapped_f64 = jax_kernel(
    _fill_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float64],
    num_outputs=3,
    in_out_argnames=["neighbor_matrix1", "neighbor_matrix_shifts1", "num_neighbors1"],
    enable_backward=False,
)

# Selective PBC prewrapped naive neighbor matrix kernel wrappers
_jax_fill_naive_pbc_prewrapped_selective_f32 = jax_kernel(
    _fill_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_naive_pbc_prewrapped_selective_f64 = jax_kernel(
    _fill_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 naive neighbor matrix kernel wrappers
_jax_fill_naive_half_f32 = jax_kernel(
    _fill_naive_neighbor_matrix_half_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_naive_half_f64 = jax_kernel(
    _fill_naive_neighbor_matrix_half_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_naive_pbc_half_f32 = jax_kernel(
    _fill_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_naive_pbc_half_f64 = jax_kernel(
    _fill_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_naive_selective_half_f32 = jax_kernel(
    _fill_naive_neighbor_matrix_selective_half_kernels[wp.float32],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_naive_selective_half_f64 = jax_kernel(
    _fill_naive_neighbor_matrix_selective_half_kernels[wp.float64],
    num_outputs=2,
    in_out_argnames=["neighbor_matrix1", "num_neighbors1"],
    enable_backward=False,
)
_jax_fill_naive_pbc_selective_half_f32 = jax_kernel(
    _fill_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_naive_pbc_selective_half_f64 = jax_kernel(
    _fill_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_naive_pbc_prewrapped_half_f32 = jax_kernel(
    _fill_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_naive_pbc_prewrapped_half_f64 = jax_kernel(
    _fill_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_naive_pbc_prewrapped_selective_half_f32 = jax_kernel(
    _fill_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_naive_pbc_prewrapped_selective_half_f64 = jax_kernel(
    _fill_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 kernel wrappers (no PBC).  Returns 4 outputs: neighbor_matrix,
# num_neighbors, neighbor_vectors, neighbor_distances.
_jax_fill_naive_pair_f32 = jax_kernel(
    _fill_naive_pair_kernels[wp.float32],
    num_outputs=4,
    in_out_argnames=[
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)
_jax_fill_naive_pair_f64 = jax_kernel(
    _fill_naive_pair_kernels[wp.float64],
    num_outputs=4,
    in_out_argnames=[
        "neighbor_matrix1",
        "num_neighbors1",
        "neighbor_vectors",
        "neighbor_distances",
    ],
    enable_backward=False,
)

# Pair-output kernel wrappers (PBC, wrap-on-entry mode).  Returns 5 outputs:
# adds neighbor_matrix_shifts.
_jax_fill_naive_pbc_pair_f32 = jax_kernel(
    _fill_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_naive_pbc_pair_f64 = jax_kernel(
    _fill_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_naive_pair_half_f32 = jax_kernel(
    _fill_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_naive_pair_half_f64 = jax_kernel(
    _fill_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_naive_pbc_pair_half_f32 = jax_kernel(
    _fill_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_naive_pbc_pair_half_f64 = jax_kernel(
    _fill_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_naive_pair_kernel(
    wp_dtype, pbc_mode: str, half_fill: bool = False, partial: bool = False
):
    """Build a geometry-output naive kernel for optional partial rows."""
    kernel = _get_naive_kernel(
        wp_dtype,
        pbc_mode=pbc_mode,
        batched=False,
        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_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 naive kernel.

    The naive kernel signature always carries ``pair_params`` / ``pair_energies`` /
    ``pair_forces`` slots; specializing the factory with ``pair_fn`` flips the
    compile-time ``HAS_PAIR_FN`` constant on so the body actually evaluates the user
    function and writes the energy/force buffers.  Here we register those two buffers
    as additional outputs (so JAX returns them).

    Cached by ``(pair_fn identity, wp_dtype, pbc_mode)`` — Warp ``@wp.func`` objects
    are hashable by identity, so a module-scope singleton ``pair_fn`` recompiles only
    once.  ``jax_kernel`` (rather than ``jax_callable``) mirrors the geometry-only
    pair-output path above: the kernel is fully specialized, so no launcher closure is
    needed.
    """
    kernel = _get_naive_kernel(
        wp_dtype,
        pbc_mode=pbc_mode,
        batched=False,
        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 single kernel wrappers
_jax_wrap_positions_single_f32 = jax_kernel(
    get_wrap_positions_kernel(wp.float32, pbc_aware=True),
    num_outputs=2,
    in_out_argnames=["positions_wrapped", "per_atom_cell_offsets"],
    enable_backward=False,
)
_jax_wrap_positions_single_f64 = jax_kernel(
    get_wrap_positions_kernel(wp.float64, pbc_aware=True),
    num_outputs=2,
    in_out_argnames=["positions_wrapped", "per_atom_cell_offsets"],
    enable_backward=False,
)


def _reset_graph_neighbor_outputs(
    neighbor_matrix,
    num_neighbors,
    fill_value,
    neighbor_matrix_shifts=None,
) -> None:
    """Reset neighbor outputs inside the Warp callback to keep buffers stable."""
    neighbor_matrix.fill_(fill_value)
    num_neighbors.zero_()
    if neighbor_matrix_shifts is not None:
        neighbor_matrix_shifts.zero_()


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_),
    )


def _wp_scalar_sentinels(wp_dtype: type, device):
    """Return Warp zero-size placeholders for inactive naive scalar inputs."""
    vec_dtype, mat_dtype = DTYPE_INFO_ALL[wp_dtype]
    return (
        empty_sentinel(1, wp.vec3i, device),
        empty_sentinel(1, mat_dtype, device),
        empty_sentinel(1, wp.vec3i, device),
        empty_sentinel(1, wp.int32, device),
        empty_sentinel(1, wp.int32, device),
        empty_sentinel(1, wp.int32, device),
        empty_sentinel(1, wp.int32, device),
        empty_sentinel(2, wp.int32, device),
        empty_sentinel(2, wp.vec3i, device),
        empty_sentinel(1, wp.int32, device),
        empty_sentinel(2, vec_dtype, device),
        empty_sentinel(2, wp_dtype, device),
        empty_sentinel(2, wp_dtype, device),
        empty_sentinel(2, wp_dtype, device),
        empty_sentinel(2, vec_dtype, device),
        empty_sentinel(1, wp.bool, device),
    )


def _run_graph_naive_no_pbc(
    positions,
    neighbor_matrix,
    num_neighbors,
    cutoff_sq,
    fill_value,
    half_fill,
    wp_dtype,
    fill_kernel,
    selective_kernel=None,
    rebuild_flags=None,
) -> None:
    """Execute the no-PBC graph-mode body."""
    total_atoms = positions.shape[0]
    (
        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,
    ) = _wp_scalar_sentinels(wp_dtype, num_neighbors.device)
    if rebuild_flags is None:
        _reset_graph_neighbor_outputs(neighbor_matrix, num_neighbors, fill_value)
        active_kernel = fill_kernel
        rebuild_flags_arg = empty_rebuild_flags
    else:
        selective_zero_num_neighbors_single(
            num_neighbors, rebuild_flags, str(num_neighbors.device)
        )
        active_kernel = selective_kernel
        rebuild_flags_arg = rebuild_flags
    wp.launch(
        kernel=active_kernel,
        dim=(1, 1, total_atoms),
        inputs=[
            positions,
            empty_offsets,
            cutoff_sq,
            wp_dtype(0.0),
            empty_cell,
            empty_shift_range,
            empty_num_shifts,
            empty_batch_idx,
            empty_batch_ptr,
            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,
            rebuild_flags_arg,
        ],
    )


def _run_graph_naive_pbc_prewrapped(
    positions,
    cell,
    shift_range,
    neighbor_matrix,
    neighbor_matrix_shifts,
    num_neighbors,
    cutoff_sq,
    num_shifts,
    fill_value,
    half_fill,
    wp_dtype,
    fill_kernel,
    selective_kernel=None,
    rebuild_flags=None,
) -> None:
    """Execute the prewrapped-PBC graph-mode body."""
    launch_dims = (1, num_shifts, positions.shape[0])
    (
        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,
    ) = _wp_scalar_sentinels(wp_dtype, num_neighbors.device)
    if rebuild_flags is None:
        _reset_graph_neighbor_outputs(
            neighbor_matrix,
            num_neighbors,
            fill_value,
            neighbor_matrix_shifts,
        )
        active_kernel = fill_kernel
        rebuild_flags_arg = empty_rebuild_flags
    else:
        selective_zero_num_neighbors_single(
            num_neighbors, rebuild_flags, str(num_neighbors.device)
        )
        active_kernel = selective_kernel
        rebuild_flags_arg = rebuild_flags

    wp.launch(
        kernel=active_kernel,
        dim=launch_dims,
        inputs=[
            positions,
            empty_offsets,
            cutoff_sq,
            wp_dtype(0.0),
            cell,
            shift_range,
            empty_num_shifts,
            empty_batch_idx,
            empty_batch_ptr,
            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,
            rebuild_flags_arg,
        ],
    )


def _run_graph_naive_pbc_wrapped(
    positions,
    cell,
    inv_cell,
    pbc,
    shift_range,
    positions_wrapped,
    per_atom_cell_offsets,
    neighbor_matrix,
    neighbor_matrix_shifts,
    num_neighbors,
    cutoff_sq,
    num_shifts,
    fill_value,
    half_fill,
    wp_dtype,
    wrap_kernel,
    fill_kernel,
    selective_kernel=None,
    rebuild_flags=None,
) -> None:
    """Execute the wrapped-PBC graph-mode body."""
    total_atoms = positions.shape[0]
    launch_dims = (1, num_shifts, total_atoms)
    (
        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,
    ) = _wp_scalar_sentinels(wp_dtype, num_neighbors.device)
    if rebuild_flags is None:
        _reset_graph_neighbor_outputs(
            neighbor_matrix,
            num_neighbors,
            fill_value,
            neighbor_matrix_shifts,
        )
        active_kernel = fill_kernel
        rebuild_flags_arg = empty_rebuild_flags
    else:
        selective_zero_num_neighbors_single(
            num_neighbors, rebuild_flags, str(num_neighbors.device)
        )
        active_kernel = selective_kernel
        rebuild_flags_arg = rebuild_flags

    wp.launch(
        kernel=wrap_kernel,
        dim=total_atoms,
        inputs=[positions, cell, inv_cell, pbc, wp.empty((0,), dtype=wp.int32)],
        outputs=[positions_wrapped, per_atom_cell_offsets],
    )

    wp.launch(
        kernel=active_kernel,
        dim=launch_dims,
        inputs=[
            positions_wrapped,
            per_atom_cell_offsets,
            cutoff_sq,
            wp_dtype(0.0),
            cell,
            shift_range,
            empty_num_shifts,
            empty_batch_idx,
            empty_batch_ptr,
            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,
            rebuild_flags_arg,
        ],
    )


def _graph_naive_no_pbc_f32(
    positions: wp.array(dtype=wp.vec3f),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff_sq: wp.float32,
    fill_value: wp.int32,
    half_fill: wp.bool,
) -> None:
    _run_graph_naive_no_pbc(
        positions,
        neighbor_matrix,
        num_neighbors,
        cutoff_sq,
        fill_value,
        half_fill,
        wp.float32,
        _fill_naive_neighbor_matrix_kernels[wp.float32],
    )


def _graph_naive_no_pbc_f64(
    positions: wp.array(dtype=wp.vec3d),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff_sq: wp.float64,
    fill_value: wp.int32,
    half_fill: wp.bool,
) -> None:
    _run_graph_naive_no_pbc(
        positions,
        neighbor_matrix,
        num_neighbors,
        cutoff_sq,
        fill_value,
        half_fill,
        wp.float64,
        _fill_naive_neighbor_matrix_kernels[wp.float64],
    )


def _graph_naive_no_pbc_selective_f32(
    positions: wp.array(dtype=wp.vec3f),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff_sq: wp.float32,
    fill_value: wp.int32,
    half_fill: wp.bool,
    rebuild_flags: wp.array(dtype=wp.bool),
) -> None:
    _run_graph_naive_no_pbc(
        positions,
        neighbor_matrix,
        num_neighbors,
        cutoff_sq,
        fill_value,
        half_fill,
        wp.float32,
        _fill_naive_neighbor_matrix_kernels[wp.float32],
        selective_kernel=_fill_naive_neighbor_matrix_selective_kernels[wp.float32],
        rebuild_flags=rebuild_flags,
    )


def _graph_naive_no_pbc_selective_f64(
    positions: wp.array(dtype=wp.vec3d),
    neighbor_matrix: wp.array(dtype=wp.int32, ndim=2),
    num_neighbors: wp.array(dtype=wp.int32),
    cutoff_sq: wp.float64,
    fill_value: wp.int32,
    half_fill: wp.bool,
    rebuild_flags: wp.array(dtype=wp.bool),
) -> None:
    _run_graph_naive_no_pbc(
        positions,
        neighbor_matrix,
        num_neighbors,
        cutoff_sq,
        fill_value,
        half_fill,
        wp.float64,
        _fill_naive_neighbor_matrix_kernels[wp.float64],
        selective_kernel=_fill_naive_neighbor_matrix_selective_kernels[wp.float64],
        rebuild_flags=rebuild_flags,
    )


def _graph_naive_pbc_prewrapped_f32(
    positions: wp.array(dtype=wp.vec3f),
    cell: wp.array(dtype=wp.mat33f),
    shift_range: wp.array(dtype=wp.vec3i),
    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_sq: wp.float32,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
) -> None:
    _run_graph_naive_pbc_prewrapped(
        positions,
        cell,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float32,
        _fill_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float32],
    )


def _graph_naive_pbc_prewrapped_f64(
    positions: wp.array(dtype=wp.vec3d),
    cell: wp.array(dtype=wp.mat33d),
    shift_range: wp.array(dtype=wp.vec3i),
    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_sq: wp.float64,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
) -> None:
    _run_graph_naive_pbc_prewrapped(
        positions,
        cell,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float64,
        _fill_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float64],
    )


def _graph_naive_pbc_prewrapped_selective_f32(
    positions: wp.array(dtype=wp.vec3f),
    cell: wp.array(dtype=wp.mat33f),
    shift_range: wp.array(dtype=wp.vec3i),
    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_sq: wp.float32,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
    rebuild_flags: wp.array(dtype=wp.bool),
) -> None:
    _run_graph_naive_pbc_prewrapped(
        positions,
        cell,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float32,
        _fill_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float32],
        selective_kernel=_fill_naive_neighbor_matrix_pbc_prewrapped_selective_kernels[
            wp.float32
        ],
        rebuild_flags=rebuild_flags,
    )


def _graph_naive_pbc_prewrapped_selective_f64(
    positions: wp.array(dtype=wp.vec3d),
    cell: wp.array(dtype=wp.mat33d),
    shift_range: wp.array(dtype=wp.vec3i),
    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_sq: wp.float64,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
    rebuild_flags: wp.array(dtype=wp.bool),
) -> None:
    _run_graph_naive_pbc_prewrapped(
        positions,
        cell,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float64,
        _fill_naive_neighbor_matrix_pbc_prewrapped_kernels[wp.float64],
        selective_kernel=_fill_naive_neighbor_matrix_pbc_prewrapped_selective_kernels[
            wp.float64
        ],
        rebuild_flags=rebuild_flags,
    )


def _graph_naive_pbc_wrapped_f32(
    positions: wp.array(dtype=wp.vec3f),
    cell: wp.array(dtype=wp.mat33f),
    inv_cell: wp.array(dtype=wp.mat33f),
    pbc: wp.array2d(dtype=wp.bool),
    shift_range: wp.array(dtype=wp.vec3i),
    positions_wrapped: wp.array(dtype=wp.vec3f),
    per_atom_cell_offsets: wp.array(dtype=wp.vec3i),
    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_sq: wp.float32,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
) -> None:
    _run_graph_naive_pbc_wrapped(
        positions,
        cell,
        inv_cell,
        pbc,
        shift_range,
        positions_wrapped,
        per_atom_cell_offsets,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float32,
        get_wrap_positions_kernel(wp.float32, pbc_aware=True),
        _fill_naive_neighbor_matrix_pbc_kernels[wp.float32],
    )


def _graph_naive_pbc_wrapped_f64(
    positions: wp.array(dtype=wp.vec3d),
    cell: wp.array(dtype=wp.mat33d),
    inv_cell: wp.array(dtype=wp.mat33d),
    pbc: wp.array2d(dtype=wp.bool),
    shift_range: wp.array(dtype=wp.vec3i),
    positions_wrapped: wp.array(dtype=wp.vec3d),
    per_atom_cell_offsets: wp.array(dtype=wp.vec3i),
    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_sq: wp.float64,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
) -> None:
    _run_graph_naive_pbc_wrapped(
        positions,
        cell,
        inv_cell,
        pbc,
        shift_range,
        positions_wrapped,
        per_atom_cell_offsets,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float64,
        get_wrap_positions_kernel(wp.float64, pbc_aware=True),
        _fill_naive_neighbor_matrix_pbc_kernels[wp.float64],
    )


def _graph_naive_pbc_wrapped_selective_f32(
    positions: wp.array(dtype=wp.vec3f),
    cell: wp.array(dtype=wp.mat33f),
    inv_cell: wp.array(dtype=wp.mat33f),
    pbc: wp.array2d(dtype=wp.bool),
    shift_range: wp.array(dtype=wp.vec3i),
    positions_wrapped: wp.array(dtype=wp.vec3f),
    per_atom_cell_offsets: wp.array(dtype=wp.vec3i),
    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_sq: wp.float32,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
    rebuild_flags: wp.array(dtype=wp.bool),
) -> None:
    _run_graph_naive_pbc_wrapped(
        positions,
        cell,
        inv_cell,
        pbc,
        shift_range,
        positions_wrapped,
        per_atom_cell_offsets,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float32,
        get_wrap_positions_kernel(wp.float32, pbc_aware=True),
        _fill_naive_neighbor_matrix_pbc_kernels[wp.float32],
        selective_kernel=_fill_naive_neighbor_matrix_pbc_selective_kernels[wp.float32],
        rebuild_flags=rebuild_flags,
    )


def _graph_naive_pbc_wrapped_selective_f64(
    positions: wp.array(dtype=wp.vec3d),
    cell: wp.array(dtype=wp.mat33d),
    inv_cell: wp.array(dtype=wp.mat33d),
    pbc: wp.array2d(dtype=wp.bool),
    shift_range: wp.array(dtype=wp.vec3i),
    positions_wrapped: wp.array(dtype=wp.vec3d),
    per_atom_cell_offsets: wp.array(dtype=wp.vec3i),
    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_sq: wp.float64,
    num_shifts: wp.int32,
    fill_value: wp.int32,
    half_fill: wp.bool,
    rebuild_flags: wp.array(dtype=wp.bool),
) -> None:
    _run_graph_naive_pbc_wrapped(
        positions,
        cell,
        inv_cell,
        pbc,
        shift_range,
        positions_wrapped,
        per_atom_cell_offsets,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        cutoff_sq,
        num_shifts,
        fill_value,
        half_fill,
        wp.float64,
        get_wrap_positions_kernel(wp.float64, pbc_aware=True),
        _fill_naive_neighbor_matrix_pbc_kernels[wp.float64],
        selective_kernel=_fill_naive_neighbor_matrix_pbc_selective_kernels[wp.float64],
        rebuild_flags=rebuild_flags,
    )


_GRAPH_NAIVE_NO_PBC_IN_OUT_ARGS = ("neighbor_matrix", "num_neighbors")
_GRAPH_NAIVE_PBC_IN_OUT_ARGS = (
    "neighbor_matrix",
    "neighbor_matrix_shifts",
    "num_neighbors",
)
_GRAPH_NAIVE_PBC_WRAPPED_IN_OUT_ARGS = (
    "positions_wrapped",
    "per_atom_cell_offsets",
    "neighbor_matrix",
    "neighbor_matrix_shifts",
    "num_neighbors",
)
_GRAPH_NAIVE_DTYPE_TO_WARP_CALLABLES = {
    (False, False): {
        "num_outputs": 2,
        "in_out_argnames": _GRAPH_NAIVE_NO_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_no_pbc_f32,
        jnp.dtype(jnp.float64): _graph_naive_no_pbc_f64,
    },
    (False, True): {
        "num_outputs": 2,
        "in_out_argnames": _GRAPH_NAIVE_NO_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_no_pbc_selective_f32,
        jnp.dtype(jnp.float64): _graph_naive_no_pbc_selective_f64,
    },
    (True, False, False): {
        "num_outputs": 3,
        "in_out_argnames": _GRAPH_NAIVE_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_pbc_prewrapped_f32,
        jnp.dtype(jnp.float64): _graph_naive_pbc_prewrapped_f64,
    },
    (True, False, True): {
        "num_outputs": 3,
        "in_out_argnames": _GRAPH_NAIVE_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_pbc_prewrapped_selective_f32,
        jnp.dtype(jnp.float64): _graph_naive_pbc_prewrapped_selective_f64,
    },
    (True, True, False): {
        "num_outputs": 5,
        "in_out_argnames": _GRAPH_NAIVE_PBC_WRAPPED_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_pbc_wrapped_f32,
        jnp.dtype(jnp.float64): _graph_naive_pbc_wrapped_f64,
    },
    (True, True, True): {
        "num_outputs": 5,
        "in_out_argnames": _GRAPH_NAIVE_PBC_WRAPPED_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_pbc_wrapped_selective_f32,
        jnp.dtype(jnp.float64): _graph_naive_pbc_wrapped_selective_f64,
    },
}


def _register_graph_naive_callables() -> dict[
    tuple[bool, bool, bool, jnp.dtype], object
]:
    """Register GraphMode.WARP callables for all naive graph-mode paths."""
    registered: dict[tuple[bool, bool, bool, jnp.dtype], object] = {}

    for key, spec in _GRAPH_NAIVE_DTYPE_TO_WARP_CALLABLES.items():
        if len(key) == 2:
            has_pbc, selective = key
            wrap_positions_values = (False, True)
        else:
            has_pbc, wrap_positions, selective = key
            wrap_positions_values = (wrap_positions,)

        for dtype in (jnp.dtype(jnp.float32), jnp.dtype(jnp.float64)):
            callable_obj = jax_callable(
                spec[dtype],
                num_outputs=spec["num_outputs"],
                in_out_argnames=spec["in_out_argnames"],
                graph_mode=GraphMode.WARP,
            )
            for wrap_positions in wrap_positions_values:
                registered[(has_pbc, wrap_positions, selective, dtype)] = callable_obj

    return registered


_GRAPH_NAIVE_WARP_CALLABLES = _register_graph_naive_callables()


# ==============================================================================
# Tiled-kernel callables (``strategy="tile"``, CUDA-only)
# ==============================================================================
#
# These wrap the *inner* warp launchers ``_launch_naive_neighbor_matrix_no_pbc``
# / ``_launch_naive_neighbor_matrix_pbc`` inside a ``jax_callable`` body and
# pass ``strategy="tile"`` explicitly, so the tile-cooperative
# ``wp.launch_tiled`` kernel is honored unconditionally (unlike the high-level
# ``naive_neighbor_matrix`` launchers, which drop ``strategy`` on the
# non-pair branch and would only reach tile via the "auto" heuristic).
#
# The inner launchers own the 2D tile ``dim`` math (``[1, N]`` no-PBC,
# ``[num_shifts, N]`` PBC), the BLOCK_DIM, the scalar sentinels, and the
# internal wrap launch for the wrapped-PBC case, so the JAX bodies stay thin.
#
# These run only on the eager (``graph_mode="none"``) path, where
# ``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.  ``graph_mode="warp"`` + tile is
# rejected up-front (those pre-fills are skipped under warp).
#
# Tile supports no-PBC and PBC (wrapped + prewrapped) and ``half_fill``; it has
# no pair-output / ``target_indices`` / selective specialization.  The static
# scalars (``cutoff``, ``half_fill``, and ``num_shifts`` for PBC) are already
# host-static in the existing naive graph path — no new host sync.


def _graph_naive_tile_no_pbc_f32(
    positions: wp.array(dtype=wp.vec3f),
    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=False,
        half_fill=bool(half_fill),
        strategy="tile",
    )


def _graph_naive_tile_no_pbc_f64(
    positions: wp.array(dtype=wp.vec3d),
    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=False,
        half_fill=bool(half_fill),
        strategy="tile",
    )


# Prewrapped PBC kernels consume precomputed shift ranges; ``pbc`` only affects
# position wrapping, so these callables intentionally omit it.
def _graph_naive_tile_pbc_prewrapped_f32(
    positions: wp.array(dtype=wp.vec3f),
    cell: wp.array(dtype=wp.mat33f),
    shift_range: wp.array(dtype=wp.vec3i),
    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,
    num_shifts: wp.int32,
    half_fill: wp.bool,
) -> None:
    _launch_naive_neighbor_matrix_pbc(
        positions,
        float(cutoff),
        cell,
        None,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        wp.float32,
        str(positions.device),
        batched=False,
        num_shifts=int(num_shifts),
        half_fill=bool(half_fill),
        wrap_positions=False,
        strategy="tile",
    )


def _graph_naive_tile_pbc_prewrapped_f64(
    positions: wp.array(dtype=wp.vec3d),
    cell: wp.array(dtype=wp.mat33d),
    shift_range: wp.array(dtype=wp.vec3i),
    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,
    num_shifts: wp.int32,
    half_fill: wp.bool,
) -> None:
    _launch_naive_neighbor_matrix_pbc(
        positions,
        float(cutoff),
        cell,
        None,
        shift_range,
        neighbor_matrix,
        neighbor_matrix_shifts,
        num_neighbors,
        wp.float64,
        str(positions.device),
        batched=False,
        num_shifts=int(num_shifts),
        half_fill=bool(half_fill),
        wrap_positions=False,
        strategy="tile",
    )


def _graph_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),
    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,
    num_shifts: 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=False,
        num_shifts=int(num_shifts),
        half_fill=bool(half_fill),
        wrap_positions=True,
        strategy="tile",
    )


def _graph_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),
    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,
    num_shifts: 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=False,
        num_shifts=int(num_shifts),
        half_fill=bool(half_fill),
        wrap_positions=True,
        strategy="tile",
    )


# Keyed by ``(has_pbc, wrap_positions)``.  Tile has no selective variant, so the
# selective axis is omitted here; ``strategy="tile"`` rejects
# ``rebuild_flags`` at the dispatch site.
_GRAPH_NAIVE_TILE_NO_PBC_IN_OUT_ARGS = ("neighbor_matrix", "num_neighbors")
_GRAPH_NAIVE_TILE_PBC_IN_OUT_ARGS = (
    "neighbor_matrix",
    "neighbor_matrix_shifts",
    "num_neighbors",
)
_GRAPH_NAIVE_TILE_SPECS = {
    (False, False): {
        "num_outputs": 2,
        "in_out_argnames": _GRAPH_NAIVE_TILE_NO_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_tile_no_pbc_f32,
        jnp.dtype(jnp.float64): _graph_naive_tile_no_pbc_f64,
    },
    (True, False): {
        "num_outputs": 3,
        "in_out_argnames": _GRAPH_NAIVE_TILE_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_tile_pbc_prewrapped_f32,
        jnp.dtype(jnp.float64): _graph_naive_tile_pbc_prewrapped_f64,
    },
    (True, True): {
        "num_outputs": 3,
        "in_out_argnames": _GRAPH_NAIVE_TILE_PBC_IN_OUT_ARGS,
        jnp.dtype(jnp.float32): _graph_naive_tile_pbc_wrapped_f32,
        jnp.dtype(jnp.float64): _graph_naive_tile_pbc_wrapped_f64,
    },
}


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

    ``GraphMode.NONE`` (not WARP): the tile bodies assume the caller has
    already pre-filled the output buffers, which only the eager
    (``graph_mode="none"``) path of ``naive_neighbor_list`` does.
    """
    registered: dict[tuple[bool, bool, jnp.dtype], object] = {}
    for (has_pbc, wrap_positions), spec in _GRAPH_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


_GRAPH_NAIVE_TILE_CALLABLES = _register_graph_naive_tile_callables()


def _naive_pair_outputs_forward(
    positions: jax.Array,
    cell: jax.Array | None,
    *,
    pbc: jax.Array | None,
    cutoff: float,
    max_neighbors: int,
    fill_value: 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,
    max_shifts_per_system: int | 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 naive autograd path.

    Detaches positions/cell, runs the pair-output naive kernel, and
    packs the indices the autograd primitive needs for the reconstruction
    backward.

    When ``pair_fn`` is set, a ``pair_fn``-specialized kernel is launched and the
    per-pair ``pair_energies`` / ``pair_forces`` are appended to
    :attr:`_NeighborForwardOutput.extra_outputs` (positions 4 and 5).  These ride
    along *outside* the ``custom_vjp`` primitive: ``positions`` is detached above, so
    they are autograd-constants (forward-only / zero cotangent), while
    ``distances`` / ``vectors`` are re-attached on the original positions.
    """
    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)
    zero_dt = float(0.0)
    (
        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.
    # (JAX is functional, so user-supplied energy/force buffers cannot be written
    # in-place; we always allocate fresh and return them — the return contract
    # matches torch, the in-place-buffer aspect does not.)
    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_naive_pair_fn_kernel(
                pair_fn, wp_dtype, "none", half_fill, is_partial
            )
        elif is_partial:
            kernel = _get_jax_naive_pair_kernel(wp_dtype, "none", half_fill, is_partial)
        elif half_fill:
            kernel = (
                _jax_fill_naive_pair_half_f64 if f64 else _jax_fill_naive_pair_half_f32
            )
        else:
            kernel = _jax_fill_naive_pair_f64 if f64 else _jax_fill_naive_pair_f32
        outs = kernel(
            positions,
            empty_offsets,
            cutoff_sq,
            zero_dt,
            empty_cell,
            empty_shift_range,
            empty_num_shifts,
            empty_batch_idx,
            empty_batch_ptr,
            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_naive_pair_fn_kernel(
                pair_fn, wp_dtype, "wrap_on_entry", half_fill, is_partial
            )
        elif is_partial:
            kernel = _get_jax_naive_pair_kernel(
                wp_dtype, "wrap_on_entry", half_fill, is_partial
            )
        elif half_fill:
            kernel = (
                _jax_fill_naive_pbc_pair_half_f64
                if f64
                else _jax_fill_naive_pbc_pair_half_f32
            )
        else:
            kernel = (
                _jax_fill_naive_pbc_pair_f64 if f64 else _jax_fill_naive_pbc_pair_f32
            )
        if cell.ndim == 2:
            cell = cell[jnp.newaxis, :, :]
        if pbc.ndim == 1:
            pbc = pbc[jnp.newaxis, :]
        # ``max_shifts`` sizes the middle launch axis: the single-system PBC kernel
        # derives each periodic image from ``ishift = wp.tid()`` (no internal shift
        # loop), so the launch must enumerate every shift.  Pinning it to 1 would
        # silently drop all non-zero images (only ``ishift == 0`` runs), matching
        # neighbors only in the R==1 regime.
        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(cell, cutoff, pbc)
        offs = jnp.zeros((total_atoms, 3), dtype=jnp.int32)
        active_shift_dim = int(max_shifts_per_system)
        if is_partial and not half_fill:
            active_shift_dim = 2 * active_shift_dim - 1
        outs = kernel(
            positions,
            offs,
            cutoff_sq,
            zero_dt,
            cell,
            shift_range_per_dimension,
            num_shifts_per_system,
            empty_batch_idx,
            empty_batch_ptr,
            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, active_shift_dim, num_rows),
        )
        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=None,
        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 naive_neighbor_list( positions: jax.Array, cutoff: float, cell: jax.Array | None = None, pbc: 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, rebuild_flags: jax.Array | None = None, wrap_positions: bool = True, inv_cell_buffer: jax.Array | None = None, positions_wrapped_buffer: jax.Array | None = None, per_atom_cell_offsets_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, # Pair-output / partial kwargs. 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, # Deprecated kwarg aliases (removed in 0.5): inv_cell: jax.Array | None = None, positions_wrapped: jax.Array | None = None, per_atom_cell_offsets: jax.Array | None = None, graph_mode: Literal["none", "warp"] = "none", ) -> ( tuple[jax.Array, jax.Array, jax.Array, jax.Array] | tuple[jax.Array, jax.Array, jax.Array] | tuple[jax.Array, jax.Array] ): """Compute neighbor list using naive O(N^2) algorithm. Identifies all atom pairs within a specified cutoff distance 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 Atomic coordinates in Cartesian space. Each row represents one atom's (x, y, z) position. cutoff : float Cutoff distance for neighbor detection in Cartesian units. Must be positive. Atoms within this distance are considered neighbors. pbc : jax.Array, shape (3,) or (1, 3), dtype=bool, optional Periodic boundary condition flags for each dimension. True enables periodicity in that direction. Default is None (no PBC). cell : jax.Array, shape (1, 3, 3), dtype=float32 or float64, optional Cell matrices defining lattice vectors in Cartesian coordinates. Required if pbc is provided. Default is None. max_neighbors : int, optional Maximum number of neighbors per atom. Must be positive. If exceeded, excess neighbors are ignored. Must be provided if neighbor_matrix is not provided. half_fill : bool, optional If True, only store relationships where i < j to avoid double counting. If False, store all neighbor relationships symmetrically. 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), dtype=int32, optional Neighbor matrix to be filled. Pass in a pre-shaped array to hint buffer reuse to XLA; note that JAX returns a new array rather than mutating the input. ``num_rows`` is ``total_atoms`` normally and ``len(target_indices)`` when partial rows are requested. Must be provided if max_neighbors is not provided. neighbor_matrix_shifts : jax.Array, shape (num_rows, max_neighbors, 3), dtype=int32, optional Shift vectors for each neighbor relationship. Pass in a pre-shaped array to hint buffer reuse to XLA; note that JAX returns a new array rather than mutating the input. Must be provided if max_neighbors is not provided. num_neighbors : jax.Array, shape (num_rows,), dtype=int32, optional Number of neighbors found for each atom. Pass in a pre-shaped array to hint buffer reuse to XLA; note that JAX returns a new array rather than mutating the input. Must be provided if max_neighbors is not provided. shift_range_per_dimension : jax.Array, shape (1, 3), dtype=int32, optional Shift range in each dimension for each system. Pass in a pre-computed value to avoid recomputation for PBC systems. num_shifts_per_system : jax.Array, shape (1,), dtype=int32, optional Number of periodic shifts for the system. Pass in a pre-computed value to avoid recomputation for PBC systems. max_shifts_per_system : int, optional Maximum per-system shift count. Pass in a pre-computed value to avoid recomputation for PBC systems. return_neighbor_list : bool, optional - default = False If True, convert the neighbor matrix to a neighbor list (idx_i, idx_j) format by creating a mask over the fill_value, which can incur a performance penalty. 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. 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 has no pair-output / ``target_indices`` / selective (``rebuild_flags``) variant and is not supported with ``graph_mode="warp"`` in this binding; requesting any of those with ``strategy="tile"`` raises. ``"auto"`` preserves the current JAX behavior (scalar dispatch) and never selects tile — tile is opt-in in the JAX binding (unlike the torch single-system binding, whose ``"auto"`` tiles by default). The tile and scalar paths produce identical pair *sets* (per-row ordering may differ). inv_cell : jax.Array, shape (1, 3, 3), dtype matches positions, optional Inverse cell matrix consumed by the wrap kernel. Only used when ``pbc`` is provided and ``wrap_positions=True``. Pass in a precomputed value to avoid a per-call ``jnp.linalg.inv`` and to keep the input pointer stable for ``graph_mode="warp"`` graph replay (omitting it forces cache-miss-per-call on the wrapped path). If None, computed from ``cell`` each call. The shape must be exactly ``(1, 3, 3)`` (matching the internally-normalized ``cell``); a ``(3, 3)`` array would silently allocate a different buffer per call and break ``graph_mode="warp"`` cache replay, which is why a mismatched shape now raises ``ValueError``. positions_wrapped : jax.Array, shape (total_atoms, 3), dtype matches positions, optional Scratch buffer the wrap kernel writes into. Pass in a pre-shaped array to keep the buffer pointer stable across ``graph_mode="warp"`` calls (required for graph-replay cache hits on the wrapped path). If None, allocated fresh each call. A mismatched shape or dtype raises ``ValueError`` to prevent silent graph-replay cache misses. per_atom_cell_offsets : jax.Array, shape (total_atoms, 3), dtype=int32, optional Scratch buffer the wrap kernel uses to record per-atom cell offsets. Pass in a pre-shaped array to keep the buffer pointer stable for ``graph_mode="warp"`` replay. If None, allocated fresh each call. A mismatched shape or dtype raises ``ValueError`` to prevent silent graph-replay cache misses. graph_mode : {"none", "warp"}, default="none" Execution mode for the underlying Warp launches. ``"none"`` preserves the existing per-kernel ``jax_kernel`` dispatch path. ``"warp"`` uses fused ``jax_callable(..., graph_mode=GraphMode.WARP)`` callbacks and is intended for ``jax.jit`` call sites that donate reusable output buffers. Returns ------- results : tuple of jax.Array Variable-length tuple depending on input parameters. The return pattern follows: - No PBC, matrix format: ``(neighbor_matrix, num_neighbors)`` - No PBC, list format: ``(neighbor_list, neighbor_ptr)`` - With PBC, matrix format: ``(neighbor_matrix, num_neighbors, neighbor_matrix_shifts)`` - With PBC, list format: ``(neighbor_list, neighbor_ptr, neighbor_list_shifts)`` **Components returned:** - **neighbor_data** (array): Neighbor indices, format depends on ``return_neighbor_list``: * If ``return_neighbor_list=False`` (default): Returns ``neighbor_matrix`` with shape (num_rows, max_neighbors), dtype int32. Row ``r`` contains neighbors for atom ``r`` or ``target_indices[r]`` when partial rows are requested. * If ``return_neighbor_list=True``: Returns ``neighbor_list`` with shape (2, num_pairs), dtype int32, in COO format [source_rows, target_atoms]. With ``target_indices``, source rows are compact row ids. - **num_neighbor_data** (array): Information about the number of neighbors for each atom, format depends on ``return_neighbor_list``: * If ``return_neighbor_list=False`` (default): Returns ``num_neighbors`` with shape (num_rows,), dtype int32. Count of neighbors found for each atom. Always returned. * If ``return_neighbor_list=True``: Returns ``neighbor_ptr`` with shape (num_rows + 1,), dtype int32. CSR-style pointer arrays where ``neighbor_ptr_data[i]`` to ``neighbor_ptr_data[i+1]`` gives the range of neighbors for row i in the flattened neighbor list. - **neighbor_shift_data** (array, optional): Periodic shift vectors, only when ``pbc`` is provided: format depends on ``return_neighbor_list``: * If ``return_neighbor_list=False`` (default): Returns ``neighbor_matrix_shifts`` with shape (num_rows, max_neighbors, 3), dtype int32. * If ``return_neighbor_list=True``: Returns ``unit_shifts`` with shape (num_pairs, 3), dtype int32. Examples -------- Basic usage without periodic boundary conditions: >>> import jax.numpy as jnp >>> from nvalchemiops.jax.neighbors import compute_naive_num_shifts, naive_neighbor_list >>> positions = jnp.zeros((100, 3), dtype=jnp.float32) >>> cutoff = 2.5 >>> max_neighbors = 50 >>> neighbor_matrix, num_neighbors = naive_neighbor_list( ... positions, cutoff, max_neighbors=max_neighbors ... ) With periodic boundary conditions: >>> cell = jnp.eye(3, dtype=jnp.float32).reshape(1, 3, 3) * 10.0 >>> pbc = jnp.array([[True, True, True]]) >>> neighbor_matrix, num_neighbors, shifts = naive_neighbor_list( ... positions, cutoff, max_neighbors=max_neighbors, pbc=pbc, cell=cell ... ) Return as neighbor list instead of matrix: >>> neighbor_list, neighbor_ptr = naive_neighbor_list( ... positions, cutoff, max_neighbors=max_neighbors, return_neighbor_list=True ... ) >>> source_atoms, target_atoms = neighbor_list[0], neighbor_list[1] Warp graph replay with donated buffers (PBC + wrap_positions=True): >>> import functools >>> import jax >>> # Pre-allocate the wrap kernel's scratch buffers and inv_cell once. >>> # Capturing them in the closure (rather than donating) keeps their >>> # buffer pointers stable across calls, which is what Warp's graph >>> # cache keys on. Only the buffers naive_neighbor_list returns are >>> # donated, so the in/out arity of the jit'ed step matches. >>> inv_cell = jnp.linalg.inv(cell) >>> positions_wrapped = jnp.zeros_like(positions) >>> per_atom_cell_offsets = jnp.zeros((positions.shape[0], 3), dtype=jnp.int32) >>> shift_range, num_shifts_per_system, max_shifts_per_system = ( ... compute_naive_num_shifts(cell, cutoff, pbc) ... ) >>> @functools.partial(jax.jit, donate_argnums=(1, 2, 3)) ... def md_step(positions, neighbor_matrix, num_neighbors, shifts): ... return naive_neighbor_list( ... positions, ... cutoff, ... cell=cell, ... pbc=pbc, ... neighbor_matrix=neighbor_matrix, ... num_neighbors=num_neighbors, ... neighbor_matrix_shifts=shifts, ... inv_cell=inv_cell, ... positions_wrapped=positions_wrapped, ... per_atom_cell_offsets=per_atom_cell_offsets, ... shift_range_per_dimension=shift_range, ... num_shifts_per_system=num_shifts_per_system, ... max_shifts_per_system=max_shifts_per_system, ... graph_mode="warp", ... ) See Also -------- nvalchemiops.neighbors.naive.naive_neighbor_matrix : Core warp launcher (no PBC) nvalchemiops.neighbors.naive.naive_neighbor_matrix_pbc : Core warp launcher (with PBC) cell_list : O(N) cell list method for larger systems Notes ----- For lower host-side launch overhead on supported GPUs, setting ``XLA_FLAGS=--xla_gpu_enable_command_buffer=CUSTOM_CALL`` before importing JAX can improve the steady-state ``graph_mode="none"`` and ``graph_mode="warp"`` paths. Advanced users can bound Warp's graph cache via ``warp.jax_experimental.set_jax_callable_default_graph_cache_max(...)``. For ``graph_mode="warp"`` to actually replay (rather than re-capture every call), every in/out buffer pointer the fused callable sees must be stable across calls. The output buffers (``neighbor_matrix``, ``num_neighbors``, ``neighbor_matrix_shifts`` when applicable) must be user-provided **and** included in ``donate_argnums`` of the enclosing ``jax.jit`` so they round-trip across calls. On the wrapped path (``pbc`` provided + ``wrap_positions=True``), ``inv_cell``, ``positions_wrapped`` and ``per_atom_cell_offsets`` must also be passed in with stable buffer pointers; the simplest way is to pre-allocate them once and capture them in the jit'ed closure (see the example above). Letting any of these allocate fresh inside ``naive_neighbor_list`` silently degrades the wrapped path to cold-capture-per-call (correct, but significantly slower than the proposal's measured replay numbers). """ graph_mode = _validate_graph_mode(graph_mode) if strategy not in {"auto", "scalar", "tile"}: raise ValueError( f"strategy must be 'auto' | 'scalar' | 'tile', got {strategy!r}", ) # ``pair_fn`` requires per-atom ``pair_params``. Note: under JAX (functional # arrays) any user-supplied ``pair_energies`` / ``pair_forces`` cannot be written # in-place — they are auto-allocated and returned, so the *return* contract # matches torch while the in-place-buffer aspect does not. 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.") 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, # selective (rebuild_flags), or CUDA-graph (graph_mode="warp") variant. # Gate here, before any launch, mirroring the warp launcher CPU guard. 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 graph_mode != "none": raise NotImplementedError( "strategy='tile' is only supported with " "graph_mode='none'; CUDA-graph capture of the tile kernel is a " "follow-up.", ) 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 graph_mode != "none" or rebuild_flags is not None: raise NotImplementedError( "Pair outputs require graph_mode='none' and no 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, ) if cell is not None and cell.ndim == 2: cell_norm = cell[jnp.newaxis, :, :] else: cell_norm = cell if cell_norm is not None and cell_norm.dtype != positions.dtype: cell_norm = cell_norm.astype(positions.dtype) pbc_norm = None if pbc is not None: pbc_norm = pbc if pbc.ndim == 2 else pbc[jnp.newaxis, :] 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 naive_neighbor_list under jax.jit with PBC and " "target_indices / pair outputs.", ) from exc forward_kwargs = { "pbc": pbc_norm, "cutoff": float(cutoff), "max_neighbors": int(max_neighbors), "fill_value": int(fill_value), "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, "max_shifts_per_system": max_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, _naive_pair_outputs_forward, forward_kwargs, ) # ``extra_outputs`` carries the per-pair energy/force tail only when # ``pair_fn`` is set, so the route return is 5 elements (geometry only) or 7. 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 COO order aligned # with ``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 (torch/.../naive.py): 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, :] # Resolve deprecated unsuffixed kwarg aliases. inv_cell = resolve_buffer_alias( "inv_cell_buffer", inv_cell_buffer, "inv_cell", inv_cell, ) positions_wrapped = resolve_buffer_alias( "positions_wrapped_buffer", positions_wrapped_buffer, "positions_wrapped", positions_wrapped, ) per_atom_cell_offsets = resolve_buffer_alias( "per_atom_cell_offsets_buffer", per_atom_cell_offsets_buffer, "per_atom_cell_offsets", per_atom_cell_offsets, ) # Validate caller-supplied scratch buffers used by the wrap kernel. Shape # or dtype mismatches would silently break graph_mode="warp" cache replay # by changing input buffer pointers/layouts on every call, so reject them # early with a clear error. if inv_cell is not None: if inv_cell.shape != (1, 3, 3): raise ValueError( f"inv_cell must have shape (1, 3, 3) to match the internal " f"cell layout; got {inv_cell.shape}. A mismatched shape " f"silently breaks graph_mode='warp' cache replay." ) if inv_cell.dtype != positions.dtype: raise ValueError( f"inv_cell dtype must match positions dtype " f"({positions.dtype}); got {inv_cell.dtype}." ) if positions_wrapped is not None: expected_pw_shape = (positions.shape[0], 3) if positions_wrapped.shape != expected_pw_shape: raise ValueError( f"positions_wrapped must have shape {expected_pw_shape}; " f"got {positions_wrapped.shape}." ) if positions_wrapped.dtype != positions.dtype: raise ValueError( f"positions_wrapped dtype must match positions dtype " f"({positions.dtype}); got {positions_wrapped.dtype}." ) if per_atom_cell_offsets is not None: expected_off_shape = (positions.shape[0], 3) if per_atom_cell_offsets.shape != expected_off_shape: raise ValueError( f"per_atom_cell_offsets must have shape {expected_off_shape}; " f"got {per_atom_cell_offsets.shape}." ) if per_atom_cell_offsets.dtype != jnp.int32: raise ValueError( f"per_atom_cell_offsets dtype must be int32; " f"got {per_atom_cell_offsets.dtype}." ) if max_neighbors is None and ( neighbor_matrix is None or (neighbor_matrix_shifts is None and pbc is not None) or num_neighbors is None ): max_neighbors = estimate_max_neighbors(cutoff) if fill_value is None: fill_value = 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 and graph_mode == "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 and graph_mode == "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 and graph_mode == "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 rebuild_flags is None and graph_mode == "warp": neighbor_matrix = neighbor_matrix.at[:].set(fill_value) num_neighbors = num_neighbors.at[:].set(jnp.int32(0)) if pbc is not None: neighbor_matrix_shifts = neighbor_matrix_shifts.at[:].set(jnp.int32(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_naive_half_f64 _jax_fill_pbc = _jax_fill_naive_pbc_half_f64 _jax_fill_pbc_prewrapped = _jax_fill_naive_pbc_prewrapped_half_f64 _jax_fill_selective = _jax_fill_naive_selective_half_f64 _jax_fill_pbc_selective = _jax_fill_naive_pbc_selective_half_f64 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_naive_pbc_prewrapped_selective_half_f64 ) else: _jax_fill = _jax_fill_naive_f64 _jax_fill_pbc = _jax_fill_naive_pbc_f64 _jax_fill_pbc_prewrapped = _jax_fill_naive_pbc_prewrapped_f64 _jax_fill_selective = _jax_fill_naive_selective_f64 _jax_fill_pbc_selective = _jax_fill_naive_pbc_selective_f64 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_naive_pbc_prewrapped_selective_f64 ) _jax_wrap_single = _jax_wrap_positions_single_f64 else: if half_fill: _jax_fill = _jax_fill_naive_half_f32 _jax_fill_pbc = _jax_fill_naive_pbc_half_f32 _jax_fill_pbc_prewrapped = _jax_fill_naive_pbc_prewrapped_half_f32 _jax_fill_selective = _jax_fill_naive_selective_half_f32 _jax_fill_pbc_selective = _jax_fill_naive_pbc_selective_half_f32 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_naive_pbc_prewrapped_selective_half_f32 ) else: _jax_fill = _jax_fill_naive_f32 _jax_fill_pbc = _jax_fill_naive_pbc_f32 _jax_fill_pbc_prewrapped = _jax_fill_naive_pbc_prewrapped_f32 _jax_fill_selective = _jax_fill_naive_selective_f32 _jax_fill_pbc_selective = _jax_fill_naive_pbc_selective_f32 _jax_fill_pbc_prewrapped_selective = ( _jax_fill_naive_pbc_prewrapped_selective_f32 ) _jax_wrap_single = _jax_wrap_positions_single_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] 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) if strategy == "tile": # CUDA-only tile-cooperative path (eager / graph_mode="none" only). # Output buffers were already pre-filled above; the callable bodies # call the inner warp launchers with strategy="tile" and rely on # the host-static cutoff / half_fill / num_shifts scalars (no new sync). cutoff_static = float(cutoff) if pbc is None: tile_callable = _GRAPH_NAIVE_TILE_CALLABLES[(False, False, positions.dtype)] neighbor_matrix, num_neighbors = tile_callable( positions, neighbor_matrix, num_neighbors, cutoff_static, half_fill, ) else: if cell.dtype != positions.dtype: cell = cell.astype(positions.dtype) num_shifts = int(max_shifts_per_system) tile_callable = _GRAPH_NAIVE_TILE_CALLABLES[ (True, bool(wrap_positions), positions.dtype) ] pbc_arg = (pbc,) if wrap_positions else () neighbor_matrix, neighbor_matrix_shifts, num_neighbors = tile_callable( positions, cell, *pbc_arg, shift_range_per_dimension, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, cutoff_static, num_shifts, half_fill, ) elif graph_mode == "warp": has_pbc = pbc is not None is_selective = rebuild_flags is not None graph_callable = _GRAPH_NAIVE_WARP_CALLABLES[ (has_pbc, wrap_positions, is_selective, positions.dtype) ] fill_value_i32 = int(fill_value) rf = None if is_selective: rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) if not has_pbc: if is_selective: neighbor_matrix, num_neighbors = graph_callable( positions, neighbor_matrix, num_neighbors, cutoff_sq, fill_value_i32, half_fill, rf, ) else: neighbor_matrix, num_neighbors = graph_callable( positions, neighbor_matrix, num_neighbors, cutoff_sq, fill_value_i32, half_fill, ) else: if cell.dtype != positions.dtype: cell = cell.astype(positions.dtype) num_shifts = int(max_shifts_per_system) if wrap_positions: if inv_cell is None: inv_cell = jnp.linalg.inv(cell) if positions_wrapped is None: positions_wrapped = jnp.zeros_like(positions) if per_atom_cell_offsets is None: per_atom_cell_offsets = jnp.zeros((total_atoms, 3), dtype=jnp.int32) if is_selective: ( positions_wrapped, per_atom_cell_offsets, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, ) = graph_callable( positions, cell, inv_cell, pbc, shift_range_per_dimension, positions_wrapped, per_atom_cell_offsets, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, cutoff_sq, num_shifts, fill_value_i32, half_fill, rf, ) else: ( positions_wrapped, per_atom_cell_offsets, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, ) = graph_callable( positions, cell, inv_cell, pbc, shift_range_per_dimension, positions_wrapped, per_atom_cell_offsets, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, cutoff_sq, num_shifts, fill_value_i32, half_fill, ) else: if is_selective: neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( graph_callable( positions, cell, shift_range_per_dimension, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, cutoff_sq, num_shifts, fill_value_i32, half_fill, rf, ) ) else: neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( graph_callable( positions, cell, shift_range_per_dimension, neighbor_matrix, neighbor_matrix_shifts, num_neighbors, cutoff_sq, num_shifts, fill_value_i32, half_fill, ) ) elif pbc is None: # No PBC case if rebuild_flags is not None: rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) num_neighbors = jnp.where( rf[0], jnp.zeros_like(num_neighbors), num_neighbors ) neighbor_matrix, num_neighbors = _jax_fill_selective( positions, empty_offsets, cutoff_sq, 0.0, empty_cell, empty_shift_range, empty_num_shifts, empty_batch_idx, empty_batch_ptr, 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, cutoff_sq, 0.0, empty_cell, empty_shift_range, empty_num_shifts, empty_batch_idx, empty_batch_ptr, 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 wrap_positions: if inv_cell is None: inv_cell = jnp.linalg.inv(cell) if positions_wrapped is None: positions_wrapped = jnp.zeros_like(positions) if per_atom_cell_offsets is None: per_atom_cell_offsets = jnp.zeros((total_atoms, 3), dtype=jnp.int32) positions_wrapped, per_atom_cell_offsets = _jax_wrap_single( positions, cell, inv_cell, pbc, jnp.empty((0,), dtype=jnp.int32), positions_wrapped, per_atom_cell_offsets, launch_dims=(total_atoms,), ) if rebuild_flags is not None: rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) num_neighbors = jnp.where( rf[0], jnp.zeros_like(num_neighbors), num_neighbors ) neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( _jax_fill_pbc_selective( positions_wrapped, per_atom_cell_offsets, cutoff_sq, 0.0, cell, shift_range_per_dimension, empty_num_shifts, empty_batch_idx, empty_batch_ptr, 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=(1, max_shifts_per_system, total_atoms), ) ) else: neighbor_matrix, neighbor_matrix_shifts, num_neighbors = _jax_fill_pbc( positions_wrapped, per_atom_cell_offsets, cutoff_sq, 0.0, cell, shift_range_per_dimension, empty_num_shifts, empty_batch_idx, empty_batch_ptr, 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=(1, max_shifts_per_system, total_atoms), ) else: if rebuild_flags is not None: rf = rebuild_flags.flatten()[:1].astype(jnp.bool_) num_neighbors = jnp.where( rf[0], jnp.zeros_like(num_neighbors), num_neighbors ) neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( _jax_fill_pbc_prewrapped_selective( positions, empty_offsets, cutoff_sq, 0.0, cell, shift_range_per_dimension, empty_num_shifts, empty_batch_idx, empty_batch_ptr, 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=(1, max_shifts_per_system, total_atoms), ) ) else: neighbor_matrix, neighbor_matrix_shifts, num_neighbors = ( _jax_fill_pbc_prewrapped( positions, empty_offsets, cutoff_sq, 0.0, cell, shift_range_per_dimension, empty_num_shifts, empty_batch_idx, empty_batch_ptr, 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=(1, max_shifts_per_system, total_atoms), ) ) 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