Source code for nvalchemiops.interactions.electrostatics.coulomb

# 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.

"""
Coulomb Electrostatic Interactions - Warp Kernel Implementation
===============================================================

This module implements direct Coulomb energy and force calculations for electrostatic
interactions using Warp GPU/CPU kernels. Includes both undamped (direct) and
damped (Ewald/PME real-space) variants.

Architecture
------------
This module provides two layers:

1. **Warp Kernels** (pure Warp, framework-agnostic):
   - ``_coulomb_energy_kernel``, ``_coulomb_energy_forces_kernel``
   - ``_coulomb_energy_matrix_kernel``, ``_coulomb_energy_forces_matrix_kernel``
   - Batch variants of all above

2. **Warp Launchers** (framework-agnostic API):
   - ``coulomb_energy()``, ``coulomb_energy_forces()``
   - ``coulomb_energy_matrix()``, ``coulomb_energy_forces_matrix()``
   - Batch variants of all above

For PyTorch integration, see ``nvalchemiops.torch.interactions.electrostatics.coulomb``.

Mathematical Formulation
------------------------

1. Coulomb Energy (Undamped):
   The energy between two charges :math:`q_i` and :math:`q_j` separated by distance r is:

   .. math::

       E_{ij} = \\frac{q_i q_j}{r}

2. Coulomb Force (Undamped):

   .. math::

       F_{ij} = \\frac{q_i q_j}{r^2} \\hat{r}

   where :math:`\\hat{r} = r_{ij} / |r_{ij}|` is the unit vector from j to i.

3. Damped Coulomb (Ewald/PME Real-Space):
   For Ewald splitting with parameter :math:`\\alpha`:

   Energy:

   .. math::

       E_{ij} = q_i q_j \\frac{\\text{erfc}(\\alpha r)}{r}

   Force:

   .. math::

       F_{ij} = q_i q_j \\left[\\frac{\\text{erfc}(\\alpha r)}{r^2} + \\frac{2\\alpha}{\\sqrt{\\pi}} \\frac{\\exp(-\\alpha^2 r^2)}{r}\\right] \\hat{r}

   where erfc(x) is the complementary error function.

.. note::
   This implementation assumes a **half neighbor list** where each pair (i, j)
   appears only once (i.e., only for i < j or only for i > j). If using a
   symmetric neighbor list where both (i, j) and (j, i) appear, the total
   energy will be doubled.

Neighbor Formats
----------------

This module supports two neighbor formats:

1. **Neighbor List (CSR format)**: ``idx_j`` is shape (num_pairs,) containing
   destination indices, ``neighbor_ptr`` is shape (N+1,) with CSR row pointers.

2. **Neighbor Matrix**: ``neighbor_matrix`` is shape (N, max_neighbors) where
   each row contains neighbor indices for that atom.

References
----------
- Allen & Tildesley, "Computer Simulation of Liquids" (1987)
- Essmann et al., J. Chem. Phys. 103, 8577 (1995) - PME paper
"""

from __future__ import annotations

import math

import warp as wp

from nvalchemiops.math import wp_erfc

__all__ = [
    # Warp launchers (framework-agnostic public API)
    "coulomb_energy",
    "coulomb_energy_forces",
    "coulomb_energy_matrix",
    "coulomb_energy_forces_matrix",
    "batch_coulomb_energy",
    "batch_coulomb_energy_forces",
    "batch_coulomb_energy_matrix",
    "batch_coulomb_energy_forces_matrix",
]

# Mathematical constants
PI = math.pi
SQRT_PI = math.sqrt(PI)
TWO_OVER_SQRT_PI = 2.0 / SQRT_PI


# ==============================================================================
# Warp Kernels - Energy Only (Neighbor List Format)
# ==============================================================================


@wp.kernel
def _coulomb_energy_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    idx_j: wp.array(dtype=wp.int32),
    neighbor_ptr: wp.array(dtype=wp.int32),
    unit_shifts: wp.array(dtype=wp.vec3i),
    cutoff: wp.float64,
    alpha: wp.float64,
    energies: wp.array(dtype=wp.float64),
):
    """Compute Coulomb energies (damped or undamped based on alpha).

    Formula (undamped, alpha=0):

    .. math::

        E_{ij} = \\frac{1}{2} \\frac{q_i q_j}{r}

    Formula (damped, alpha>0):

    .. math::

        E_{ij} = \\frac{1}{2} q_i q_j \\frac{\\text{erfc}(\\alpha r)}{r}

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors using CSR format.

    Note: Uses atomic_add to accumulate to per-atom energies.
    """
    atom_i = wp.tid()
    num_atoms = positions.shape[0]

    if atom_i >= num_atoms:
        return

    ri = positions[atom_i]
    qi = charges[atom_i]
    cell_t = wp.transpose(cell[0])

    energy_acc = wp.float64(0.0)

    j_start = neighbor_ptr[atom_i]
    j_end = neighbor_ptr[atom_i + 1]

    for edge_idx in range(j_start, j_end):
        j = idx_j[edge_idx]

        rj = positions[j]
        qj = charges[j]

        shift_vec = cell_t * type(ri)(unit_shifts[edge_idx])
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            # Damped: E = q_i * q_j * erfc(alpha*r) / r
            alpha_r = alpha * r
            erfc_term = wp_erfc(alpha_r)
            energy_acc += prefactor * erfc_term / r
        else:
            # Undamped: E = q_i * q_j / r
            energy_acc += prefactor / r

    wp.atomic_add(energies, atom_i, energy_acc)


@wp.kernel
def _coulomb_energy_forces_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    idx_j: wp.array(dtype=wp.int32),
    neighbor_ptr: wp.array(dtype=wp.int32),
    unit_shifts: wp.array(dtype=wp.vec3i),
    cutoff: wp.float64,
    alpha: wp.float64,
    energies: wp.array(dtype=wp.float64),
    forces: wp.array(dtype=wp.vec3d),
):
    """Compute Coulomb energies and forces (damped or undamped based on alpha).

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors using CSR format.

    Note: Uses atomic_add to accumulate to per-atom arrays.
    """
    atom_i = wp.tid()
    num_atoms = positions.shape[0]

    if atom_i >= num_atoms:
        return

    ri = positions[atom_i]
    qi = charges[atom_i]
    cell_t = wp.transpose(cell[0])

    energy_acc = wp.float64(0.0)
    force_acc = wp.vec3d(wp.float64(0.0), wp.float64(0.0), wp.float64(0.0))

    j_start = neighbor_ptr[atom_i]
    j_end = neighbor_ptr[atom_i + 1]

    for edge_idx in range(j_start, j_end):
        j = idx_j[edge_idx]

        rj = positions[j]
        qj = charges[j]

        shift_vec = cell_t * type(ri)(unit_shifts[edge_idx])
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            # Damped
            alpha_r = alpha * r
            alpha_r_sq = alpha_r * alpha_r
            erfc_term = wp_erfc(alpha_r)
            exp_term = wp.exp(-alpha_r_sq)

            # Energy: E = q_i * q_j * erfc(alphar) / r
            energy_acc += prefactor * erfc_term / r

            # Force: F = q_i * q_j *
            # [erfc(alpha*r)/r^3 + 2*alpha/sqrt(pi) *
            # exp(-alpha^2*r^2)/r^2] * r_ij
            two_over_sqrt_pi = wp.float64(1.1283791670955126)
            force_mag_over_r = erfc_term / (
                r * r * r
            ) + two_over_sqrt_pi * alpha * exp_term / (r * r)
            force_ij = prefactor * force_mag_over_r * r_ij
        else:
            # Undamped: E = q_i * q_j / r, F = q_i * q_j / r^3 * r_ij
            energy_acc += prefactor / r
            force_mag_over_r = prefactor / (r * r * r)
            force_ij = force_mag_over_r * r_ij

        # Accumulate force on i, apply Newton's 3rd law to j
        force_acc += force_ij
        wp.atomic_add(forces, j, -force_ij)

    wp.atomic_add(energies, atom_i, energy_acc)
    wp.atomic_add(forces, atom_i, force_acc)


# ==============================================================================
# Warp Kernels - Neighbor Matrix Format
# ==============================================================================


@wp.kernel
def _coulomb_energy_matrix_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    neighbor_matrix: wp.array2d(dtype=wp.int32),
    neighbor_matrix_shifts: wp.array2d(dtype=wp.vec3i),
    cutoff: wp.float64,
    alpha: wp.float64,
    fill_value: wp.int32,
    atomic_energies: wp.array(dtype=wp.float64),
):
    """Compute Coulomb energies using neighbor matrix format.

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors.
    """
    atom_idx = wp.tid()
    num_atoms = positions.shape[0]
    max_neighbors = neighbor_matrix.shape[1]

    if atom_idx >= num_atoms:
        return

    ri = positions[atom_idx]
    qi = charges[atom_idx]
    cell_t = wp.transpose(cell[0])

    energy_acc = wp.float64(0.0)

    for neighbor_slot in range(max_neighbors):
        j = neighbor_matrix[atom_idx, neighbor_slot]
        if j >= fill_value or j >= num_atoms:
            continue

        rj = positions[j]
        qj = charges[j]

        shift = neighbor_matrix_shifts[atom_idx, neighbor_slot]
        shift_vec = cell_t * type(ri)(shift)
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            alpha_r = alpha * r
            erfc_term = wp_erfc(alpha_r)
            energy_acc += prefactor * erfc_term / r
        else:
            energy_acc += prefactor / r

    wp.atomic_add(atomic_energies, atom_idx, energy_acc)


@wp.kernel
def _coulomb_energy_forces_matrix_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    neighbor_matrix: wp.array2d(dtype=wp.int32),
    neighbor_matrix_shifts: wp.array2d(dtype=wp.vec3i),
    cutoff: wp.float64,
    alpha: wp.float64,
    fill_value: wp.int32,
    atomic_energies: wp.array(dtype=wp.float64),
    atomic_forces: wp.array(dtype=wp.vec3d),
):
    """Compute Coulomb energies and forces using neighbor matrix format.

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors.
    """
    atom_idx = wp.tid()
    num_atoms = positions.shape[0]
    max_neighbors = neighbor_matrix.shape[1]

    if atom_idx >= num_atoms:
        return

    ri = positions[atom_idx]
    qi = charges[atom_idx]
    cell_t = wp.transpose(cell[0])

    energy_acc = wp.float64(0.0)
    force_acc = wp.vec3d(wp.float64(0.0), wp.float64(0.0), wp.float64(0.0))

    for neighbor_slot in range(max_neighbors):
        j = neighbor_matrix[atom_idx, neighbor_slot]
        if j >= fill_value or j >= num_atoms:
            continue

        rj = positions[j]
        qj = charges[j]

        shift = neighbor_matrix_shifts[atom_idx, neighbor_slot]
        shift_vec = cell_t * type(ri)(shift)
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            alpha_r = alpha * r
            alpha_r_sq = alpha_r * alpha_r
            erfc_term = wp_erfc(alpha_r)
            exp_term = wp.exp(-alpha_r_sq)

            energy_acc += prefactor * erfc_term / r
            two_over_sqrt_pi = wp.float64(1.1283791670955126)
            force_mag_over_r = erfc_term / (
                r * r * r
            ) + two_over_sqrt_pi * alpha * exp_term / (r * r)
            force_ij = prefactor * force_mag_over_r * r_ij
        else:
            energy_acc += prefactor / r
            force_mag_over_r = prefactor / (r * r * r)
            force_ij = force_mag_over_r * r_ij

        force_acc += force_ij
        wp.atomic_add(atomic_forces, j, -force_ij)

    wp.atomic_add(atomic_energies, atom_idx, energy_acc)
    wp.atomic_add(atomic_forces, atom_idx, force_acc)


# ==============================================================================
# Warp Kernels - Batch Versions (Neighbor List Format)
# ==============================================================================


@wp.kernel
def _batch_coulomb_energy_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    idx_j: wp.array(dtype=wp.int32),
    neighbor_ptr: wp.array(dtype=wp.int32),
    unit_shifts: wp.array(dtype=wp.vec3i),
    batch_idx: wp.array(dtype=wp.int32),
    cutoff: wp.float64,
    alpha: wp.float64,
    energies: wp.array(dtype=wp.float64),
):
    """Compute Coulomb energies for batched systems.

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors using CSR format.

    Note: Uses atomic_add to accumulate to per-atom energies.
    """
    atom_i = wp.tid()
    num_atoms = positions.shape[0]

    if atom_i >= num_atoms:
        return

    system_id = batch_idx[atom_i]
    ri = positions[atom_i]
    qi = charges[atom_i]
    cell_t = wp.transpose(cell[system_id])

    energy_acc = wp.float64(0.0)

    j_start = neighbor_ptr[atom_i]
    j_end = neighbor_ptr[atom_i + 1]

    for edge_idx in range(j_start, j_end):
        j = idx_j[edge_idx]

        rj = positions[j]
        qj = charges[j]

        shift_vec = cell_t * type(ri)(unit_shifts[edge_idx])
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            alpha_r = alpha * r
            erfc_term = wp_erfc(alpha_r)
            energy_acc += prefactor * erfc_term / r
        else:
            energy_acc += prefactor / r

    wp.atomic_add(energies, atom_i, energy_acc)


@wp.kernel
def _batch_coulomb_energy_forces_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    idx_j: wp.array(dtype=wp.int32),
    neighbor_ptr: wp.array(dtype=wp.int32),
    unit_shifts: wp.array(dtype=wp.vec3i),
    batch_idx: wp.array(dtype=wp.int32),
    cutoff: wp.float64,
    alpha: wp.float64,
    energies: wp.array(dtype=wp.float64),
    forces: wp.array(dtype=wp.vec3d),
):
    """Compute Coulomb energies and forces for batched systems.

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors using CSR format.

    Note: Uses atomic_add to accumulate to per-atom arrays.
    """
    atom_i = wp.tid()
    num_atoms = positions.shape[0]

    if atom_i >= num_atoms:
        return

    system_id = batch_idx[atom_i]
    ri = positions[atom_i]
    qi = charges[atom_i]
    cell_t = wp.transpose(cell[system_id])

    energy_acc = wp.float64(0.0)
    force_acc = wp.vec3d(wp.float64(0.0), wp.float64(0.0), wp.float64(0.0))

    j_start = neighbor_ptr[atom_i]
    j_end = neighbor_ptr[atom_i + 1]

    for edge_idx in range(j_start, j_end):
        j = idx_j[edge_idx]

        rj = positions[j]
        qj = charges[j]

        shift_vec = cell_t * type(ri)(unit_shifts[edge_idx])
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            alpha_r = alpha * r
            alpha_r_sq = alpha_r * alpha_r
            erfc_term = wp_erfc(alpha_r)
            exp_term = wp.exp(-alpha_r_sq)

            energy_acc += prefactor * erfc_term / r

            two_over_sqrt_pi = wp.float64(1.1283791670955126)
            force_mag_over_r = erfc_term / (
                r * r * r
            ) + two_over_sqrt_pi * alpha * exp_term / (r * r)
            force_ij = prefactor * force_mag_over_r * r_ij
        else:
            energy_acc += prefactor / r
            force_mag_over_r = prefactor / (r * r * r)
            force_ij = force_mag_over_r * r_ij

        force_acc += force_ij
        wp.atomic_add(forces, j, -force_ij)

    wp.atomic_add(energies, atom_i, energy_acc)
    wp.atomic_add(forces, atom_i, force_acc)


# ==============================================================================
# Warp Kernels - Batch Versions (Neighbor Matrix Format)
# ==============================================================================


@wp.kernel
def _batch_coulomb_energy_matrix_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    neighbor_matrix: wp.array2d(dtype=wp.int32),
    neighbor_matrix_shifts: wp.array2d(dtype=wp.vec3i),
    batch_idx: wp.array(dtype=wp.int32),
    cutoff: wp.float64,
    alpha: wp.float64,
    fill_value: wp.int32,
    atomic_energies: wp.array(dtype=wp.float64),
):
    """Compute Coulomb energies for batched systems using neighbor matrix.

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors.
    """
    atom_idx = wp.tid()
    num_atoms = positions.shape[0]
    max_neighbors = neighbor_matrix.shape[1]

    if atom_idx >= num_atoms:
        return

    system_id = batch_idx[atom_idx]
    ri = positions[atom_idx]
    qi = charges[atom_idx]
    cell_t = wp.transpose(cell[system_id])

    energy_acc = wp.float64(0.0)

    for neighbor_slot in range(max_neighbors):
        j = neighbor_matrix[atom_idx, neighbor_slot]
        if j >= fill_value or j >= num_atoms:
            continue

        rj = positions[j]
        qj = charges[j]

        shift = neighbor_matrix_shifts[atom_idx, neighbor_slot]
        shift_vec = cell_t * type(ri)(shift)
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            alpha_r = alpha * r
            erfc_term = wp_erfc(alpha_r)
            energy_acc += prefactor * erfc_term / r
        else:
            energy_acc += prefactor / r

    wp.atomic_add(atomic_energies, atom_idx, energy_acc)


@wp.kernel
def _batch_coulomb_energy_forces_matrix_kernel(
    positions: wp.array(dtype=wp.vec3d),
    charges: wp.array(dtype=wp.float64),
    cell: wp.array(dtype=wp.mat33d),
    neighbor_matrix: wp.array2d(dtype=wp.int32),
    neighbor_matrix_shifts: wp.array2d(dtype=wp.vec3i),
    batch_idx: wp.array(dtype=wp.int32),
    cutoff: wp.float64,
    alpha: wp.float64,
    fill_value: wp.int32,
    atomic_energies: wp.array(dtype=wp.float64),
    atomic_forces: wp.array(dtype=wp.vec3d),
):
    """Compute Coulomb energies and forces for batched systems using neighbor matrix.

    Launch Grid: dim = [num_atoms]
    Each thread processes one atom and loops over its neighbors.
    """
    atom_idx = wp.tid()
    num_atoms = positions.shape[0]
    max_neighbors = neighbor_matrix.shape[1]

    if atom_idx >= num_atoms:
        return

    system_id = batch_idx[atom_idx]
    ri = positions[atom_idx]
    qi = charges[atom_idx]
    cell_t = wp.transpose(cell[system_id])

    energy_acc = wp.float64(0.0)
    force_acc = wp.vec3d(wp.float64(0.0), wp.float64(0.0), wp.float64(0.0))

    for neighbor_slot in range(max_neighbors):
        j = neighbor_matrix[atom_idx, neighbor_slot]
        if j >= fill_value or j >= num_atoms:
            continue

        rj = positions[j]
        qj = charges[j]

        shift = neighbor_matrix_shifts[atom_idx, neighbor_slot]
        shift_vec = cell_t * type(ri)(shift)
        r_ij = ri - rj - shift_vec
        r = wp.length(r_ij)

        if r >= cutoff or r < wp.float64(1e-10):
            continue

        prefactor = wp.float64(0.5) * qi * qj

        if alpha > wp.float64(0.0):
            alpha_r = alpha * r
            alpha_r_sq = alpha_r * alpha_r
            erfc_term = wp_erfc(alpha_r)
            exp_term = wp.exp(-alpha_r_sq)

            energy_acc += prefactor * erfc_term / r
            two_over_sqrt_pi = wp.float64(1.1283791670955126)
            force_mag_over_r = erfc_term / (
                r * r * r
            ) + two_over_sqrt_pi * alpha * exp_term / (r * r)
            force_ij = prefactor * force_mag_over_r * r_ij
        else:
            energy_acc += prefactor / r
            force_mag_over_r = prefactor / (r * r * r)
            force_ij = force_mag_over_r * r_ij

        force_acc += force_ij
        wp.atomic_add(atomic_forces, j, -force_ij)

    wp.atomic_add(atomic_energies, atom_idx, energy_acc)
    wp.atomic_add(atomic_forces, atom_idx, force_acc)


# ==============================================================================
# Warp Launchers (Framework-Agnostic)
# ==============================================================================


[docs] def coulomb_energy( positions: wp.array, charges: wp.array, cell: wp.array, idx_j: wp.array, neighbor_ptr: wp.array, unit_shifts: wp.array, cutoff: float, alpha: float, energies: wp.array, device: str | None = None, ) -> None: """Launch Coulomb energy kernel using CSR neighbor list format. This is a framework-agnostic launcher that accepts warp arrays directly. Framework-specific wrappers (PyTorch, JAX) handle tensor-to-warp conversion. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions. charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (1,), dtype=wp.mat33d Unit cell matrix. idx_j : wp.array, shape (num_pairs,), dtype=wp.int32 Destination atom indices in CSR format. neighbor_ptr : wp.array, shape (N+1,), dtype=wp.int32 CSR row pointers. unit_shifts : wp.array, shape (num_pairs,), dtype=wp.vec3i Integer unit cell shifts. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies array in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _coulomb_energy_kernel, dim=num_atoms, inputs=[ positions, charges, cell, idx_j, neighbor_ptr, unit_shifts, wp.float64(cutoff), wp.float64(alpha), energies, ], device=device, )
[docs] def coulomb_energy_forces( positions: wp.array, charges: wp.array, cell: wp.array, idx_j: wp.array, neighbor_ptr: wp.array, unit_shifts: wp.array, cutoff: float, alpha: float, energies: wp.array, forces: wp.array, device: str | None = None, ) -> None: """Launch Coulomb energy and forces kernel using CSR neighbor list format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions. charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (1,), dtype=wp.mat33d Unit cell matrix. idx_j : wp.array, shape (num_pairs,), dtype=wp.int32 Destination atom indices in CSR format. neighbor_ptr : wp.array, shape (N+1,), dtype=wp.int32 CSR row pointers. unit_shifts : wp.array, shape (num_pairs,), dtype=wp.vec3i Integer unit cell shifts. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. forces : wp.array, shape (N,), dtype=wp.vec3d OUTPUT: Per-atom forces. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies and forces arrays in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _coulomb_energy_forces_kernel, dim=num_atoms, inputs=[ positions, charges, cell, idx_j, neighbor_ptr, unit_shifts, wp.float64(cutoff), wp.float64(alpha), energies, forces, ], device=device, )
[docs] def coulomb_energy_matrix( positions: wp.array, charges: wp.array, cell: wp.array, neighbor_matrix: wp.array, neighbor_matrix_shifts: wp.array, cutoff: float, alpha: float, fill_value: int, energies: wp.array, device: str | None = None, ) -> None: """Launch Coulomb energy kernel using neighbor matrix format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions. charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (1,), dtype=wp.mat33d Unit cell matrix. neighbor_matrix : wp.array2d, shape (N, max_neighbors), dtype=wp.int32 Neighbor indices. neighbor_matrix_shifts : wp.array2d, shape (N, max_neighbors), dtype=wp.vec3i Integer unit cell shifts. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). fill_value : int Value indicating padding in neighbor_matrix. energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies array in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _coulomb_energy_matrix_kernel, dim=num_atoms, inputs=[ positions, charges, cell, neighbor_matrix, neighbor_matrix_shifts, wp.float64(cutoff), wp.float64(alpha), wp.int32(fill_value), energies, ], device=device, )
[docs] def coulomb_energy_forces_matrix( positions: wp.array, charges: wp.array, cell: wp.array, neighbor_matrix: wp.array, neighbor_matrix_shifts: wp.array, cutoff: float, alpha: float, fill_value: int, energies: wp.array, forces: wp.array, device: str | None = None, ) -> None: """Launch Coulomb energy and forces kernel using neighbor matrix format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions. charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (1,), dtype=wp.mat33d Unit cell matrix. neighbor_matrix : wp.array2d, shape (N, max_neighbors), dtype=wp.int32 Neighbor indices. neighbor_matrix_shifts : wp.array2d, shape (N, max_neighbors), dtype=wp.vec3i Integer unit cell shifts. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). fill_value : int Value indicating padding in neighbor_matrix. energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. forces : wp.array, shape (N,), dtype=wp.vec3d OUTPUT: Per-atom forces. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies and forces arrays in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _coulomb_energy_forces_matrix_kernel, dim=num_atoms, inputs=[ positions, charges, cell, neighbor_matrix, neighbor_matrix_shifts, wp.float64(cutoff), wp.float64(alpha), wp.int32(fill_value), energies, forces, ], device=device, )
[docs] def batch_coulomb_energy( positions: wp.array, charges: wp.array, cell: wp.array, idx_j: wp.array, neighbor_ptr: wp.array, unit_shifts: wp.array, batch_idx: wp.array, cutoff: float, alpha: float, energies: wp.array, device: str | None = None, ) -> None: """Launch batched Coulomb energy kernel using CSR neighbor list format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions (all systems concatenated). charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (B,), dtype=wp.mat33d Unit cell matrices for each system. idx_j : wp.array, shape (num_pairs,), dtype=wp.int32 Destination atom indices in CSR format. neighbor_ptr : wp.array, shape (N+1,), dtype=wp.int32 CSR row pointers. unit_shifts : wp.array, shape (num_pairs,), dtype=wp.vec3i Integer unit cell shifts. batch_idx : wp.array, shape (N,), dtype=wp.int32 System index for each atom. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies array in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _batch_coulomb_energy_kernel, dim=num_atoms, inputs=[ positions, charges, cell, idx_j, neighbor_ptr, unit_shifts, batch_idx, wp.float64(cutoff), wp.float64(alpha), energies, ], device=device, )
[docs] def batch_coulomb_energy_forces( positions: wp.array, charges: wp.array, cell: wp.array, idx_j: wp.array, neighbor_ptr: wp.array, unit_shifts: wp.array, batch_idx: wp.array, cutoff: float, alpha: float, energies: wp.array, forces: wp.array, device: str | None = None, ) -> None: """Launch batched Coulomb energy and forces kernel using CSR neighbor list format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions (all systems concatenated). charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (B,), dtype=wp.mat33d Unit cell matrices for each system. idx_j : wp.array, shape (num_pairs,), dtype=wp.int32 Destination atom indices in CSR format. neighbor_ptr : wp.array, shape (N+1,), dtype=wp.int32 CSR row pointers. unit_shifts : wp.array, shape (num_pairs,), dtype=wp.vec3i Integer unit cell shifts. batch_idx : wp.array, shape (N,), dtype=wp.int32 System index for each atom. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. forces : wp.array, shape (N,), dtype=wp.vec3d OUTPUT: Per-atom forces. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies and forces arrays in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _batch_coulomb_energy_forces_kernel, dim=num_atoms, inputs=[ positions, charges, cell, idx_j, neighbor_ptr, unit_shifts, batch_idx, wp.float64(cutoff), wp.float64(alpha), energies, forces, ], device=device, )
[docs] def batch_coulomb_energy_matrix( positions: wp.array, charges: wp.array, cell: wp.array, neighbor_matrix: wp.array, neighbor_matrix_shifts: wp.array, batch_idx: wp.array, cutoff: float, alpha: float, fill_value: int, energies: wp.array, device: str | None = None, ) -> None: """Launch batched Coulomb energy kernel using neighbor matrix format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions (all systems concatenated). charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (B,), dtype=wp.mat33d Unit cell matrices for each system. neighbor_matrix : wp.array2d, shape (N, max_neighbors), dtype=wp.int32 Neighbor indices. neighbor_matrix_shifts : wp.array2d, shape (N, max_neighbors), dtype=wp.vec3i Integer unit cell shifts. batch_idx : wp.array, shape (N,), dtype=wp.int32 System index for each atom. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). fill_value : int Value indicating padding in neighbor_matrix. energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies array in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _batch_coulomb_energy_matrix_kernel, dim=num_atoms, inputs=[ positions, charges, cell, neighbor_matrix, neighbor_matrix_shifts, batch_idx, wp.float64(cutoff), wp.float64(alpha), wp.int32(fill_value), energies, ], device=device, )
[docs] def batch_coulomb_energy_forces_matrix( positions: wp.array, charges: wp.array, cell: wp.array, neighbor_matrix: wp.array, neighbor_matrix_shifts: wp.array, batch_idx: wp.array, cutoff: float, alpha: float, fill_value: int, energies: wp.array, forces: wp.array, device: str | None = None, ) -> None: """Launch batched Coulomb energy and forces kernel using neighbor matrix format. Parameters ---------- positions : wp.array, shape (N,), dtype=wp.vec3d Atomic positions (all systems concatenated). charges : wp.array, shape (N,), dtype=wp.float64 Atomic charges. cell : wp.array, shape (B,), dtype=wp.mat33d Unit cell matrices for each system. neighbor_matrix : wp.array2d, shape (N, max_neighbors), dtype=wp.int32 Neighbor indices. neighbor_matrix_shifts : wp.array2d, shape (N, max_neighbors), dtype=wp.vec3i Integer unit cell shifts. batch_idx : wp.array, shape (N,), dtype=wp.int32 System index for each atom. cutoff : float Cutoff distance. alpha : float Ewald splitting parameter (0.0 for undamped). fill_value : int Value indicating padding in neighbor_matrix. energies : wp.array, shape (N,), dtype=wp.float64 OUTPUT: Per-atom energies. Must be pre-allocated and zeroed. forces : wp.array, shape (N,), dtype=wp.vec3d OUTPUT: Per-atom forces. Must be pre-allocated and zeroed. device : str, optional Warp device. If None, inferred from positions. Returns ------- None Results are written to energies and forces arrays in-place. """ num_atoms = positions.shape[0] if device is None: device = str(positions.device) wp.launch( _batch_coulomb_energy_forces_matrix_kernel, dim=num_atoms, inputs=[ positions, charges, cell, neighbor_matrix, neighbor_matrix_shifts, batch_idx, wp.float64(cutoff), wp.float64(alpha), wp.int32(fill_value), energies, forces, ], device=device, )