# 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.
"""
Unified PME Kernels
===================
This module provides GPU-accelerated Warp launchers for Particle Mesh Ewald
(PME) calculations. Green structure-factor and virial-background kernels live
here; convolve and correction launchers route through ``pme_factory.py``.
Charge assignment and force interpolation are handled by the spline module.
MATHEMATICAL FORMULATION
========================
PME splits the Coulomb energy into components:
.. math::
E_{\\text{total}} = E_{\\text{real}} + E_{\\text{reciprocal}} - E_{\\text{self}} - E_{\\text{background}}
This module provides low-level support for:
1. Green's Function and Structure Factor Correction:
.. math::
G(k) = \\frac{2\\pi}{V} \\frac{\\exp(-k^2/(4\\alpha^2))}{k^2}
The B-spline charge assignment introduces aliasing, corrected by the squared
B-spline structure factor:
.. math::
C^2(k) = \\left[\\operatorname{sinc}(m_x/N_x) \\cdot \\operatorname{sinc}(m_y/N_y)
\\cdot \\operatorname{sinc}(m_z/N_z)\\right]^{2p}
where :math:`p` is the spline order and PME deconvolution multiplies by
:math:`G(k) / C^2(k)`.
2. Factory-backed energy corrections:
- Self-energy: :math:`E_{\\text{self}} = \\frac{\\alpha}{\\sqrt{\\pi}} \\sum_i q_i^2`
- Background (for non-neutral systems): :math:`E_{\\text{background}} = \\frac{\\pi}{2\\alpha^2 V} \\sum_i q_i Q_{\\text{total}}`
DTYPE FLEXIBILITY
=================
The hand-written Green structure-factor and virial-background kernels support
both float32 and float64 inputs via explicit overloads. Factory-backed convolve
and correction launchers select typed kernels through ``get_pme_kernel``.
KERNEL ORGANIZATION
===================
Green's Function Kernels:
_pme_green_structure_factor_kernel: Single-system G(k) and C(k)
_batch_pme_green_structure_factor_kernel: Batched version
Factory-Backed Correction Launchers:
pme_energy_corrections: Single-system self + background correction
batch_pme_energy_corrections: Batched self + background correction
Internal Factory-Backed Convolve Helpers:
pme_convolve: Single-system PME reciprocal convolution
batch_pme_convolve: Batched PME reciprocal convolution
.. warning
In contrast to the other electrostatic kernels that offer end-to-end
``warp`` launchers, PME requires FFT for the convolution step that is
currently not available in ``warp``. As a result, bindings must call
FFT within their own framework in between kernel launches. The sequence
of calls looks like the following:
1. Spread charges to mesh: ``spline_spread()``
2. Forward FFT: ``framework.fft.rfftn(mesh)``
3. Legacy helper: ``pme_green_structure_factor()`` returns raw ``G(k)``
and ``C^2(k)``
4. Convolution: ``mesh_fft * green_function / structure_factor_sq``
5. Inverse FFT: ``framework.fft.irfftn(...)``
6. Gather potential: ``spline_gather()``
7. Apply corrections: ``pme_energy_corrections()``
The Torch/JAX PME paths use internal factory-backed convolve helpers that
compute the effective folded multiplier ``G(k) / C^2(k)`` inside the fused
convolve kernel.
REFERENCES
==========
- Essmann et al. (1995). J. Chem. Phys. 103, 8577 (SPME paper)
- Darden et al. (1993). J. Chem. Phys. 98, 10089 (Original PME)
- torchpme: https://github.com/lab-cosmo/torch-pme (Reference implementation)
"""
import math
from typing import Any
import warp as wp
# Mathematical constants
PI = math.pi
TWOPI = 2.0 * PI
###########################################################################################
########################### Helper Functions ##############################################
###########################################################################################
@wp.func
def compute_sinc(x: Any) -> Any:
"""Compute normalized sinc function: :math:`\\sin(\\pi x)/(\\pi x)`.
Uses Taylor expansion near zero for numerical stability.
Parameters
----------
x : Any
Scalar argument; dtype determined by the calling context (wp.float32 or wp.float64).
Returns
-------
Any
:math:`\\sin(\\pi x)/(\\pi x)`, or 1.0 when ``|x| < 1e-6``.
"""
abs_x = wp.abs(x)
one = type(x)(1.0)
threshold = type(x)(1e-6)
if abs_x < threshold:
return one
pi_x = type(x)(PI) * x
return wp.sin(pi_x) / pi_x
@wp.func
def wp_exp_kernel(k_sq: Any, prefactor: Any) -> Any:
"""Compute exp(-prefactor * k_sq) / k_sq.
Parameters
----------
k_sq : Any
Squared wave-vector magnitude; dtype determined by calling context.
prefactor : Any
Scalar prefactor in the exponent; same dtype as ``k_sq``.
Returns
-------
Any
:math:`\\exp(-\\text{prefactor} \\cdot k^2) / k^2`.
"""
return wp.exp(-prefactor * k_sq) / k_sq
###########################################################################################
########################### Green Function with Structure Factor ##########################
###########################################################################################
@wp.kernel
def _pme_green_structure_factor_kernel(
k_squared: wp.array3d(dtype=Any), # (Nx, Ny, Nz_rfft)
miller_x: wp.array(dtype=Any), # (Nx,)
miller_y: wp.array(dtype=Any), # (Ny,)
miller_z: wp.array(dtype=Any), # (Nz_rfft,)
alpha: wp.array(dtype=Any), # (1,)
volume: wp.array(dtype=Any), # (1,)
mesh_nx: wp.int32,
mesh_ny: wp.int32,
mesh_nz: wp.int32,
spline_order: wp.int32,
green_function: wp.array3d(dtype=Any), # (Nx, Ny, Nz_rfft)
structure_factor_sq: wp.array3d(dtype=Any), # (Nx, Ny, Nz_rfft)
):
r"""Compute PME Green's function and B-spline structure factor correction.
Computes two arrays needed for PME reciprocal space:
1. Green's function: :math:`G(k) = (2\pi/V) \cdot \exp(-k^2/(4\alpha^2)) / k^2`
2. Structure factor squared: :math:`|B(k)|^2` for B-spline dealiasing
The structure factor correction accounts for aliasing from B-spline
charge spreading: C(k) = [sinc(h/N_x) * sinc(k/N_y) * sinc(l/N_z)]^(2p)
Launch Grid
-----------
dim = [Nx, Ny, Nz_rfft]
Each thread processes one grid point in the FFT mesh (using rfft symmetry).
Parameters
----------
k_squared : wp.array3d, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
Squared magnitude of k-vectors at each grid point.
miller_x : wp.array, shape (Nx,), dtype=wp.float32 or wp.float64
Miller indices in x direction (from fftfreq).
miller_y : wp.array, shape (Ny,), dtype=wp.float32 or wp.float64
Miller indices in y direction (from fftfreq).
miller_z : wp.array, shape (Nz_rfft,), dtype=wp.float32 or wp.float64
Miller indices in z direction (from rfftfreq).
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume.
mesh_nx, mesh_ny, mesh_nz : wp.int32
Full mesh dimensions (Nz is the full size, not rfft size).
spline_order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended.
green_function : wp.array3d, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Green's function G(k) at each grid point.
structure_factor_sq : wp.array3d, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: :math:`|B(k)|^2` structure factor squared at each grid point.
Notes
-----
- k=0 (grid point [0,0,0]) is explicitly set to zero (tin-foil boundary conditions).
- Near-zero :math:`k^2` values are set to zero to avoid division by zero.
- Structure factor is clamped to avoid division by zero in dealiasing.
- Uses rfft symmetry: only Nz_rfft = Nz//2 + 1 points in z.
"""
i, j, k = wp.tid()
k_sq = k_squared[i, j, k]
alpha_ = alpha[0]
volume_ = volume[0]
mi_x = miller_x[i]
mi_y = miller_y[j]
mi_z = miller_z[k]
# Get dtype-specific constants
zero = type(k_sq)(0.0)
one = type(k_sq)(1.0)
four = type(k_sq)(4.0)
threshold = type(k_sq)(1e-10)
clamp_threshold = type(k_sq)(1e-10)
twopi = type(k_sq)(TWOPI)
# Structure factor: sinc(mi_x/Nx) * sinc(mi_y/Ny) * sinc(mi_z/Nz).
# This helper returns raw G(k) plus C^2(k). The folded G/C^2 path lives in
# the factory-backed fused convolve helpers.
sinc_x = compute_sinc(mi_x / type(mi_x)(mesh_nx))
sinc_y = compute_sinc(mi_y / type(mi_y)(mesh_ny))
sinc_z = compute_sinc(mi_z / type(mi_z)(mesh_nz))
sinc_product = sinc_x * sinc_y * sinc_z
# Raise to spline_order power. The loop runs up to 5 extra multiplies
# so we cover spline_order in [1, 6]. The inner `_ < spline_order` guard
# stops at the correct power for each supported order.
sf = sinc_product
for _ in range(1, 6): # supports spline_order in [1, 6]
if _ < spline_order:
sf = sf * sinc_product
# Clamp to avoid division by zero
if sf < clamp_threshold:
sf = clamp_threshold
sf_sq = sf * sf
structure_factor_sq[i, j, k] = sf_sq
# Raw volume-normalized Green's function. External callers that use this
# helper apply the B-spline deconvolution with ``structure_factor_sq``.
if k_sq < threshold:
green_function[i, j, k] = zero
else:
exp_factor = wp_exp_kernel(k_sq, one / (four * alpha_ * alpha_))
green_function[i, j, k] = twopi * exp_factor / volume_
if i == 0 and j == 0 and k == 0:
green_function[i, j, k] = zero
@wp.kernel
def _batch_pme_green_structure_factor_kernel(
k_squared: wp.array4d(dtype=Any), # (B, Nx, Ny, Nz_rfft)
miller_x: wp.array(dtype=Any), # (Nx,)
miller_y: wp.array(dtype=Any), # (Ny,)
miller_z: wp.array(dtype=Any), # (Nz_rfft,)
alpha: wp.array(dtype=Any), # (B,)
volumes: wp.array(dtype=Any), # (B,)
mesh_nx: wp.int32,
mesh_ny: wp.int32,
mesh_nz: wp.int32,
spline_order: wp.int32,
green_function: wp.array4d(dtype=Any), # (B, Nx, Ny, Nz_rfft)
structure_factor_sq: wp.array3d(dtype=Any), # (Nx, Ny, Nz_rfft)
):
r"""Compute PME Green's function and B-spline structure factor for batched systems.
Batched version of _pme_green_structure_factor_kernel. Each system can have
different alpha and volume values, but shares the same mesh dimensions.
Green's function: :math:`G_s(k) = (2\pi/V_s) \cdot \exp(-k^2/(4\alpha_s^2)) / k^2`
Structure factor: :math:`|B(k)|^2` (computed once, shared across systems)
Launch Grid
-----------
dim = [B, Nx, Ny, Nz_rfft]
Each thread processes one (system, grid_point) pair.
Parameters
----------
k_squared : wp.array4d, shape (B, Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
Per-system squared magnitude of k-vectors at each grid point.
miller_x : wp.array, shape (Nx,), dtype=wp.float32 or wp.float64
Miller indices in x direction (shared across systems).
miller_y : wp.array, shape (Ny,), dtype=wp.float32 or wp.float64
Miller indices in y direction (shared across systems).
miller_z : wp.array, shape (Nz_rfft,), dtype=wp.float32 or wp.float64
Miller indices in z direction (shared across systems).
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter.
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volume.
mesh_nx, mesh_ny, mesh_nz : wp.int32
Full mesh dimensions (Nz is the full size, not rfft size).
spline_order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended.
green_function : wp.array4d, shape (B, Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Per-system Green's function G_s(k) at each grid point.
structure_factor_sq : wp.array3d, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: :math:`|B(k)|^2` structure factor squared (computed only at batch_idx=0).
Notes
-----
- k=0 (grid point [0,0,0]) is explicitly set to zero for each system.
- Near-zero :math:`k^2` values are set to zero to avoid division by zero.
- Structure factor is computed only once (at batch_idx=0) since it depends
only on mesh dimensions and spline order, not on system parameters.
- Uses rfft symmetry: only Nz_rfft = Nz//2 + 1 points in z.
"""
batch_idx, i, j, k = wp.tid()
k_sq = k_squared[batch_idx, i, j, k]
system_alpha = alpha[batch_idx]
system_volume = volumes[batch_idx]
mi_x = miller_x[i]
mi_y = miller_y[j]
mi_z = miller_z[k]
# Get dtype-specific constants
zero = type(k_sq)(0.0)
one = type(k_sq)(1.0)
four = type(k_sq)(4.0)
threshold = type(k_sq)(1e-10)
clamp_threshold = type(k_sq)(1e-10)
twopi = type(k_sq)(TWOPI)
# Structure factor C^2(k). Written once at batch_idx=0 because it depends
# only on mesh dimensions and spline order.
sinc_x = compute_sinc(mi_x / type(mi_x)(mesh_nx))
sinc_y = compute_sinc(mi_y / type(mi_y)(mesh_ny))
sinc_z = compute_sinc(mi_z / type(mi_z)(mesh_nz))
sinc_product = sinc_x * sinc_y * sinc_z
sf = sinc_product
for _ in range(1, 6):
if _ < spline_order:
sf = sf * sinc_product
if sf < clamp_threshold:
sf = clamp_threshold
sf_sq = sf * sf
if batch_idx == wp.int32(0):
structure_factor_sq[i, j, k] = sf_sq
# Raw volume-normalized Green's function; fused convolve owns folded G/C^2.
if k_sq < threshold:
green_function[batch_idx, i, j, k] = zero
else:
exp_factor = wp_exp_kernel(k_sq, one / (four * system_alpha * system_alpha))
green_function[batch_idx, i, j, k] = twopi * exp_factor / system_volume
if i == 0 and j == 0 and k == 0:
green_function[batch_idx, i, j, k] = zero
###########################################################################################
########################### PME Virial Background Correction ##############################
###########################################################################################
#
# Non-neutral PME systems have a background charge term in the energy:
# E_bg = (π · Q² ) / (2 α² V)
# whose volume derivative gives a diagonal contribution to the virial:
# W_bg = -d E_bg / dε = -(E_bg) · I (where ε is the strain tensor)
# We subtract ``E_bg · I`` from the virial diagonal to apply that correction.
#
# Pipeline: pass 1 scatter-adds per-atom q into total_charges[batch_idx];
# pass 2 (per system) computes E_bg = π Q² / (2 α² V) and subtracts from
# the virial diagonal.
@wp.kernel(enable_backward=False)
def _pme_virial_bg_reduce_kernel(
charges: wp.array(dtype=Any), # (N,)
batch_idx: wp.array(dtype=wp.int32), # (N,) — system index per atom
total_charges: wp.array(dtype=Any), # (B,) — IN/OUT, zero-initialized by caller
):
"""Pass 1: scatter-add per-atom charges into ``total_charges[batch_idx]``."""
atom_idx = wp.tid()
s = batch_idx[atom_idx]
wp.atomic_add(total_charges, s, charges[atom_idx])
@wp.kernel(enable_backward=False)
def _pme_virial_bg_apply_kernel(
total_charges: wp.array(dtype=Any), # (B,) computed in pass 1
cell: wp.array3d(dtype=Any), # (B, 3, 3)
volume: wp.array(dtype=Any), # (B,) caller-supplied or dummy
use_supplied_volume: wp.int32,
alpha: wp.array(dtype=Any), # (B,) — per-system Ewald splitting
virial_in: wp.array3d(dtype=Any), # (B, 3, 3) input
virial_out: wp.array3d(dtype=Any), # (B, 3, 3) output = virial_in - E_bg·I
):
"""Pass 2: compute E_bg and subtract it from the virial diagonal."""
s = wp.tid()
q = total_charges[s]
a = alpha[s]
pi = type(q)(PI)
two = type(q)(2.0)
c00 = cell[s, 0, 0]
c01 = cell[s, 0, 1]
c02 = cell[s, 0, 2]
c10 = cell[s, 1, 0]
c11 = cell[s, 1, 1]
c12 = cell[s, 1, 2]
c20 = cell[s, 2, 0]
c21 = cell[s, 2, 1]
c22 = cell[s, 2, 2]
det = (
c00 * (c11 * c22 - c12 * c21)
- c01 * (c10 * c22 - c12 * c20)
+ c02 * (c10 * c21 - c11 * c20)
)
cell_volume = wp.abs(det)
volume_value = cell_volume
if use_supplied_volume != 0:
volume_value = volume[s]
e_bg = pi * q * q / (two * a * a * volume_value)
virial_out[s, 0, 0] = virial_in[s, 0, 0] - e_bg
virial_out[s, 0, 1] = virial_in[s, 0, 1]
virial_out[s, 0, 2] = virial_in[s, 0, 2]
virial_out[s, 1, 0] = virial_in[s, 1, 0]
virial_out[s, 1, 1] = virial_in[s, 1, 1] - e_bg
virial_out[s, 1, 2] = virial_in[s, 1, 2]
virial_out[s, 2, 0] = virial_in[s, 2, 0]
virial_out[s, 2, 1] = virial_in[s, 2, 1]
virial_out[s, 2, 2] = virial_in[s, 2, 2] - e_bg
# Analytic backward kernel — see launcher for the math.
@wp.kernel(enable_backward=False)
def _pme_virial_bg_backward_per_system_kernel(
grad_virial: wp.array3d(dtype=Any), # (B, 3, 3) cotangent of virial_out
total_charges: wp.array(dtype=Any), # (B,) recomputed from charges
cell: wp.array3d(dtype=Any), # (B, 3, 3)
volume: wp.array(dtype=Any), # (B,) caller-supplied or dummy
use_supplied_volume: wp.int32,
alpha: wp.array(dtype=Any), # (B,)
grad_total_charges: wp.array(dtype=Any), # (B,) OUT — dL/dQ per system
grad_alpha: wp.array(dtype=Any), # (B,) OUT — dL/dα per system
grad_cell: wp.array3d(dtype=Any), # (B, 3, 3) OUT — dL/dC
):
r"""Per-system: turn the cotangent of virial_out into per-system dL/dQ, dL/dalpha, dL/dC.
From ``virial_out[s,i,j] = virial_in[s,i,j] - delta_ij * E_bg(s)`` (where
:math:`E_{bg} = \pi Q^2 / (2 \alpha^2 V)` and ``V = |det(C)|``):
.. math::
dL/dE_{bg}(s) = -(g[s,0,0] + g[s,1,1] + g[s,2,2])
dE_{bg}/dQ = \pi Q / (\alpha^2 V)
dE_{bg}/d\alpha = -\pi Q^2 / (\alpha^3 V)
dE_{bg}/dV = -\pi Q^2 / (2 \alpha^2 V^2)
d|\det C|/dC = \text{sign}(\det C) \cdot \text{cofactor}(C) \quad \text{(Jacobi's formula)}
"""
s = wp.tid()
q = total_charges[s]
a = alpha[s]
pi = type(q)(PI)
two = type(q)(2.0)
c00 = cell[s, 0, 0]
c01 = cell[s, 0, 1]
c02 = cell[s, 0, 2]
c10 = cell[s, 1, 0]
c11 = cell[s, 1, 1]
c12 = cell[s, 1, 2]
c20 = cell[s, 2, 0]
c21 = cell[s, 2, 1]
c22 = cell[s, 2, 2]
det = (
c00 * (c11 * c22 - c12 * c21)
- c01 * (c10 * c22 - c12 * c20)
+ c02 * (c10 * c21 - c11 * c20)
)
cell_volume = wp.abs(det)
volume_value = cell_volume
if use_supplied_volume != 0:
volume_value = volume[s]
sgn = wp.sign(det)
g_diag_sum = grad_virial[s, 0, 0] + grad_virial[s, 1, 1] + grad_virial[s, 2, 2]
g_E_bg = -g_diag_sum # dL/dE_bg
a2 = a * a
a3 = a2 * a
v2 = volume_value * volume_value
dE_dQ = pi * q / (a2 * volume_value)
dE_dA = -pi * q * q / (a3 * volume_value)
dE_dV = -pi * q * q / (two * a2 * v2)
grad_total_charges[s] = g_E_bg * dE_dQ
grad_alpha[s] = g_E_bg * dE_dA
dV_dC00 = sgn * (c11 * c22 - c12 * c21)
dV_dC01 = sgn * -(c10 * c22 - c12 * c20)
dV_dC02 = sgn * (c10 * c21 - c11 * c20)
dV_dC10 = sgn * -(c01 * c22 - c02 * c21)
dV_dC11 = sgn * (c00 * c22 - c02 * c20)
dV_dC12 = sgn * -(c00 * c21 - c01 * c20)
dV_dC20 = sgn * (c01 * c12 - c02 * c11)
dV_dC21 = sgn * -(c00 * c12 - c02 * c10)
dV_dC22 = sgn * (c00 * c11 - c01 * c10)
gV = g_E_bg * dE_dV
if use_supplied_volume != 0:
grad_cell[s, 0, 0] = type(q)(0.0)
grad_cell[s, 0, 1] = type(q)(0.0)
grad_cell[s, 0, 2] = type(q)(0.0)
grad_cell[s, 1, 0] = type(q)(0.0)
grad_cell[s, 1, 1] = type(q)(0.0)
grad_cell[s, 1, 2] = type(q)(0.0)
grad_cell[s, 2, 0] = type(q)(0.0)
grad_cell[s, 2, 1] = type(q)(0.0)
grad_cell[s, 2, 2] = type(q)(0.0)
else:
grad_cell[s, 0, 0] = gV * dV_dC00
grad_cell[s, 0, 1] = gV * dV_dC01
grad_cell[s, 0, 2] = gV * dV_dC02
grad_cell[s, 1, 0] = gV * dV_dC10
grad_cell[s, 1, 1] = gV * dV_dC11
grad_cell[s, 1, 2] = gV * dV_dC12
grad_cell[s, 2, 0] = gV * dV_dC20
grad_cell[s, 2, 1] = gV * dV_dC21
grad_cell[s, 2, 2] = gV * dV_dC22
@wp.kernel(enable_backward=False)
def _pme_virial_bg_backward_per_atom_kernel(
batch_idx: wp.array(dtype=wp.int32), # (N,)
grad_total_charges: wp.array(dtype=Any), # (B,) per-system dL/dQ
grad_charges: wp.array(dtype=Any), # (N,) OUT — dL/dq_j = dL/dQ(s(j))
):
"""Per-atom: dL/dq_j = dL/dQ(s(j))."""
j = wp.tid()
s = batch_idx[j]
grad_charges[j] = grad_total_charges[s]
###########################################################################################
########################### Kernel Overloads for Dtype Flexibility ########################
###########################################################################################
# Type lists for creating overloads
_T = [wp.float32, wp.float64]
# Single-system kernel overloads
_pme_green_structure_factor_kernel_overload = {}
_pme_virial_bg_reduce_kernel_overload = {}
_pme_virial_bg_apply_kernel_overload = {}
_pme_virial_bg_backward_per_system_kernel_overload = {}
_pme_virial_bg_backward_per_atom_kernel_overload = {}
# Batch kernel overloads
_batch_pme_green_structure_factor_kernel_overload = {}
for t in _T:
# Green's function kernel overloads
_pme_green_structure_factor_kernel_overload[t] = wp.overload(
_pme_green_structure_factor_kernel,
[
wp.array3d(dtype=t), # k_squared
wp.array(dtype=t), # miller_x
wp.array(dtype=t), # miller_y
wp.array(dtype=t), # miller_z
wp.array(dtype=t), # alpha
wp.array(dtype=t), # volume
wp.int32, # mesh_nx
wp.int32, # mesh_ny
wp.int32, # mesh_nz
wp.int32, # spline_order
wp.array3d(dtype=t), # green_function
wp.array3d(dtype=t), # structure_factor_sq
],
)
_batch_pme_green_structure_factor_kernel_overload[t] = wp.overload(
_batch_pme_green_structure_factor_kernel,
[
wp.array4d(dtype=t), # k_squared
wp.array(dtype=t), # miller_x
wp.array(dtype=t), # miller_y
wp.array(dtype=t), # miller_z
wp.array(dtype=t), # alpha
wp.array(dtype=t), # volumes
wp.int32, # mesh_nx
wp.int32, # mesh_ny
wp.int32, # mesh_nz
wp.int32, # spline_order
wp.array4d(dtype=t), # green_function
wp.array3d(dtype=t), # structure_factor_sq
],
)
_pme_virial_bg_reduce_kernel_overload[t] = wp.overload(
_pme_virial_bg_reduce_kernel,
[
wp.array(dtype=t), # charges
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=t), # total_charges
],
)
_pme_virial_bg_apply_kernel_overload[t] = wp.overload(
_pme_virial_bg_apply_kernel,
[
wp.array(dtype=t), # total_charges
wp.array3d(dtype=t), # cell
wp.array(dtype=t), # volume
wp.int32, # use_supplied_volume
wp.array(dtype=t), # alpha
wp.array3d(dtype=t), # virial_in
wp.array3d(dtype=t), # virial_out
],
)
_pme_virial_bg_backward_per_system_kernel_overload[t] = wp.overload(
_pme_virial_bg_backward_per_system_kernel,
[
wp.array3d(dtype=t), # grad_virial
wp.array(dtype=t), # total_charges
wp.array3d(dtype=t), # cell
wp.array(dtype=t), # volume
wp.int32, # use_supplied_volume
wp.array(dtype=t), # alpha
wp.array(dtype=t), # grad_total_charges
wp.array(dtype=t), # grad_alpha
wp.array3d(dtype=t), # grad_cell
],
)
_pme_virial_bg_backward_per_atom_kernel_overload[t] = wp.overload(
_pme_virial_bg_backward_per_atom_kernel,
[
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=t), # grad_total_charges
wp.array(dtype=t), # grad_charges
],
)
###########################################################################################
########################### Warp Launcher Functions (wp_*) ################################
###########################################################################################
def _get_pme_factory_kernel(
wp_dtype: type,
*,
component: str,
batched: bool = False,
order: str = "forward",
charge_grad: bool = False,
) -> wp.Kernel:
"""Return a PME factory kernel without creating a module import cycle."""
from nvalchemiops.interactions.electrostatics.pme_factory import get_pme_kernel
return get_pme_kernel(
wp_dtype,
component=component,
batched=batched,
order=order,
charge_grad=charge_grad,
)
def _get_pme_factory_sentinels(wp_dtype: type, device: str) -> dict[str, wp.array]:
"""Return PME factory sentinel arrays without creating a module import cycle."""
from nvalchemiops.interactions.electrostatics.pme_factory import (
alloc_pme_sentinels,
)
return alloc_pme_sentinels(wp_dtype, device)
[docs]
def pme_green_structure_factor(
k_squared: wp.array,
miller_x: wp.array,
miller_y: wp.array,
miller_z: wp.array,
alpha: wp.array,
volume: wp.array,
mesh_nx: int,
mesh_ny: int,
mesh_nz: int,
spline_order: int,
green_function: wp.array,
structure_factor_sq: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Compute PME Green's function and B-spline structure factor correction.
Framework-agnostic launcher for single-system Green's function computation.
Note: FFT Operations Offloaded to Framework
-------------------------------------------
This helper computes raw Green's function multipliers and B-spline
deconvolution factors for PME. The internal factory-backed convolve helper
computes the effective folded multiplier ``G(k) / C^2(k)`` internally.
The complete PME reciprocal-space workflow requires FFT operations
that are not available in Warp and must be performed by the calling
framework. The typical workflow is:
1. Spread charges to mesh: spline_spread()
2. Forward FFT: framework.fft.rfftn(mesh) <-- Framework-specific
3. Compute Green's function and structure factor: pme_green_structure_factor()
4. Convolution: mesh_fft * green_function / structure_factor_sq
5. Inverse FFT: framework.fft.irfftn(...) <-- Framework-specific
6. Gather potential: spline_gather()
7. Apply corrections: pme_energy_corrections()
Parameters
----------
k_squared : wp.array, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
Squared magnitude of k-vectors at each grid point.
miller_x : wp.array, shape (Nx,), dtype=wp.float32 or wp.float64
Miller indices in x direction (from fftfreq).
miller_y : wp.array, shape (Ny,), dtype=wp.float32 or wp.float64
Miller indices in y direction (from fftfreq).
miller_z : wp.array, shape (Nz_rfft,), dtype=wp.float32 or wp.float64
Miller indices in z direction (from rfftfreq).
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume.
mesh_nx, mesh_ny, mesh_nz : int
Full mesh dimensions (Nz is the full size, not rfft size).
spline_order : int
B-spline order (1-6). Order 4 (cubic) recommended.
green_function : wp.array, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Green's function G(k) at each grid point.
structure_factor_sq : wp.array, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: :math:`|B(k)|^2` structure factor squared at each grid point.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
See Also
--------
nvalchemiops.torch.interactions.electrostatics.pme : Complete PyTorch implementation
"""
nx, ny, nz_rfft = k_squared.shape[0], k_squared.shape[1], k_squared.shape[2]
kernel = _pme_green_structure_factor_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(nx, ny, nz_rfft),
inputs=[
k_squared,
miller_x,
miller_y,
miller_z,
alpha,
volume,
wp.int32(mesh_nx),
wp.int32(mesh_ny),
wp.int32(mesh_nz),
wp.int32(spline_order),
],
outputs=[green_function, structure_factor_sq],
device=device,
)
[docs]
def batch_pme_green_structure_factor(
k_squared: wp.array,
miller_x: wp.array,
miller_y: wp.array,
miller_z: wp.array,
alpha: wp.array,
volumes: wp.array,
mesh_nx: int,
mesh_ny: int,
mesh_nz: int,
spline_order: int,
green_function: wp.array,
structure_factor_sq: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Compute PME Green's function and B-spline structure factor for batched systems.
Framework-agnostic launcher for batched Green's function computation.
Each system can have different alpha and volume values, but shares
the same mesh dimensions.
Parameters
----------
k_squared : wp.array, shape (B, Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
Per-system squared magnitude of k-vectors at each grid point.
miller_x : wp.array, shape (Nx,), dtype=wp.float32 or wp.float64
Miller indices in x direction (shared across systems).
miller_y : wp.array, shape (Ny,), dtype=wp.float32 or wp.float64
Miller indices in y direction (shared across systems).
miller_z : wp.array, shape (Nz_rfft,), dtype=wp.float32 or wp.float64
Miller indices in z direction (shared across systems).
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter.
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volume.
mesh_nx, mesh_ny, mesh_nz : int
Full mesh dimensions (Nz is the full size, not rfft size).
spline_order : int
B-spline order (1-6). Order 4 (cubic) recommended.
green_function : wp.array, shape (B, Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Per-system Green's function G_s(k) at each grid point.
structure_factor_sq : wp.array, shape (Nx, Ny, Nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: :math:`|B(k)|^2` structure factor squared (computed only at batch_idx=0).
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
See Also
--------
nvalchemiops.torch.interactions.electrostatics.pme : Complete PyTorch implementation
"""
num_systems = k_squared.shape[0]
nx, ny, nz_rfft = k_squared.shape[1], k_squared.shape[2], k_squared.shape[3]
kernel = _batch_pme_green_structure_factor_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_systems, nx, ny, nz_rfft),
inputs=[
k_squared,
miller_x,
miller_y,
miller_z,
alpha,
volumes,
wp.int32(mesh_nx),
wp.int32(mesh_ny),
wp.int32(mesh_nz),
wp.int32(spline_order),
],
outputs=[green_function, structure_factor_sq],
device=device,
)
def pme_convolve(
mesh_fft: wp.array,
k_squared: wp.array,
moduli_x: wp.array,
moduli_y: wp.array,
moduli_z: wp.array,
alpha: wp.array,
volume: wp.array,
convolved_mesh: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Fused per-k-point Green's compute + B-spline deconvolution + multiply.
Single-system. ``moduli_x/y/z`` are precomputed 1D B-spline modulus LUTs
(``sinc(m/N)^spline_order`` per miller index, one per axis); the kernel
reads three values + multiplies + squares them per (i, j, k) thread,
replacing repeated inline sinc-and-power work in each convolve launch.
Parameters
----------
mesh_fft : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
Input mesh after forward rFFT, complex values as (real, imag) pairs.
k_squared : wp.array, shape (nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Squared magnitude of k-vectors at each grid point.
moduli_x : wp.array, shape (nx,), dtype=wp.float32 or wp.float64
Precomputed 1D B-spline modulus LUT along x: ``sinc(m/Nx)^spline_order``.
moduli_y : wp.array, shape (ny,), dtype=wp.float32 or wp.float64
Precomputed 1D B-spline modulus LUT along y: ``sinc(m/Ny)^spline_order``.
moduli_z : wp.array, shape (nz_rfft,), dtype=wp.float32 or wp.float64
Precomputed 1D B-spline modulus LUT along z: ``sinc(m/Nz)^spline_order``.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume.
convolved_mesh : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Convolved mesh ``mesh_fft * G(k) / C^2(k)``. May alias ``mesh_fft``
for in-place operation.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.batch_pme_convolve` : Batched variant.
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_convolve_backward` : Backward pass.
"""
nx, ny, nz_rfft = mesh_fft.shape[0], mesh_fft.shape[1], mesh_fft.shape[2]
kernel = _get_pme_factory_kernel(wp_dtype, component="pme_convolve")
wp.launch(
kernel,
dim=(nx, ny, nz_rfft),
inputs=[
mesh_fft,
k_squared,
moduli_x,
moduli_y,
moduli_z,
alpha,
volume,
],
outputs=[convolved_mesh],
device=device,
)
def batch_pme_convolve(
mesh_fft: wp.array,
k_squared: wp.array,
moduli_x: wp.array,
moduli_y: wp.array,
moduli_z: wp.array,
alpha: wp.array,
volumes: wp.array,
convolved_mesh: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched version of ``pme_convolve``; fused Green's + B-spline deconvolution for B systems.
Parameters
----------
mesh_fft : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
Per-system input mesh after forward rFFT, complex values as (real, imag) pairs.
k_squared : wp.array, shape (B, nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Per-system squared magnitude of k-vectors at each grid point.
moduli_x : wp.array, shape (nx,), dtype=wp.float32 or wp.float64
Precomputed 1D B-spline modulus LUT along x (shared across systems).
moduli_y : wp.array, shape (ny,), dtype=wp.float32 or wp.float64
Precomputed 1D B-spline modulus LUT along y (shared across systems).
moduli_z : wp.array, shape (nz_rfft,), dtype=wp.float32 or wp.float64
Precomputed 1D B-spline modulus LUT along z (shared across systems).
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter.
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volume.
convolved_mesh : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Per-system convolved mesh ``mesh_fft * G_s(k) / C^2(k)``.
May alias ``mesh_fft`` for in-place operation.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_convolve` : Single-system variant.
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.batch_pme_convolve_backward` : Backward pass.
"""
num_systems = mesh_fft.shape[0]
nx, ny, nz_rfft = mesh_fft.shape[1], mesh_fft.shape[2], mesh_fft.shape[3]
kernel = _get_pme_factory_kernel(wp_dtype, component="pme_convolve", batched=True)
wp.launch(
kernel,
dim=(num_systems, nx, ny, nz_rfft),
inputs=[
mesh_fft,
k_squared,
moduli_x,
moduli_y,
moduli_z,
alpha,
volumes,
],
outputs=[convolved_mesh],
device=device,
)
def pme_convolve_backward(
mesh_fft: wp.array,
grad_convolved: wp.array,
k_squared: wp.array,
moduli_x: wp.array,
moduli_y: wp.array,
moduli_z: wp.array,
alpha: wp.array,
volume: wp.array,
grad_mesh_fft: wp.array,
grad_alpha: wp.array,
grad_volume: wp.array,
grad_k_squared: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Single-system backward for ``pme_convolve``.
Parameters
----------
mesh_fft : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
Forward mesh values saved from the forward pass.
grad_convolved : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
Cotangent of the convolved mesh output.
k_squared : wp.array, shape (nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Squared k-vector magnitudes from the forward pass.
moduli_x : wp.array, shape (nx,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along x from the forward pass.
moduli_y : wp.array, shape (ny,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along y from the forward pass.
moduli_z : wp.array, shape (nz_rfft,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along z from the forward pass.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter from the forward pass.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume from the forward pass.
grad_mesh_fft : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Gradient w.r.t. ``mesh_fft``.
grad_alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``alpha``. Must be zero-initialized; kernel
accumulates atomically across k-points.
grad_volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``volume``. Must be zero-initialized; kernel
accumulates atomically across k-points.
grad_k_squared : wp.array, shape (nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``k_squared``. Written elementwise; no zero-init required.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_convolve` : Corresponding forward pass.
"""
nx, ny, nz_rfft = mesh_fft.shape[0], mesh_fft.shape[1], mesh_fft.shape[2]
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_convolve", order="backward"
)
wp.launch(
kernel,
dim=(nx, ny, nz_rfft),
inputs=[
mesh_fft,
grad_convolved,
k_squared,
moduli_x,
moduli_y,
moduli_z,
alpha,
volume,
],
outputs=[grad_mesh_fft, grad_alpha, grad_volume, grad_k_squared],
device=device,
)
def batch_pme_convolve_backward(
mesh_fft: wp.array,
grad_convolved: wp.array,
k_squared: wp.array,
moduli_x: wp.array,
moduli_y: wp.array,
moduli_z: wp.array,
alpha: wp.array,
volumes: wp.array,
grad_mesh_fft: wp.array,
grad_alpha: wp.array,
grad_volumes: wp.array,
grad_k_squared: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched backward for ``batch_pme_convolve``.
Parameters
----------
mesh_fft : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
Per-system forward mesh values saved from the forward pass.
grad_convolved : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
Cotangent of the convolved mesh output.
k_squared : wp.array, shape (B, nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Per-system squared k-vector magnitudes from the forward pass.
moduli_x : wp.array, shape (nx,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along x from the forward pass.
moduli_y : wp.array, shape (ny,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along y from the forward pass.
moduli_z : wp.array, shape (nz_rfft,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along z from the forward pass.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter from the forward pass.
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volumes from the forward pass.
grad_mesh_fft : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Gradient w.r.t. ``mesh_fft``.
grad_alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``alpha``. Must be zero-initialized; kernel
accumulates atomically across k-points.
grad_volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``volumes``. Must be zero-initialized; kernel
accumulates atomically across k-points.
grad_k_squared : wp.array, shape (B, nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``k_squared``. Written elementwise; no zero-init required.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.batch_pme_convolve` : Corresponding forward pass.
"""
num_systems = mesh_fft.shape[0]
nx, ny, nz_rfft = mesh_fft.shape[1], mesh_fft.shape[2], mesh_fft.shape[3]
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_convolve", batched=True, order="backward"
)
wp.launch(
kernel,
dim=(num_systems, nx, ny, nz_rfft),
inputs=[
mesh_fft,
grad_convolved,
k_squared,
moduli_x,
moduli_y,
moduli_z,
alpha,
volumes,
],
outputs=[grad_mesh_fft, grad_alpha, grad_volumes, grad_k_squared],
device=device,
)
def pme_convolve_double_backward(
h_grad_mesh: wp.array,
h_alpha: wp.array,
h_volume: wp.array,
h_grad_ksq: wp.array,
mesh_fft: wp.array,
grad_convolved: wp.array,
k_squared: wp.array,
moduli_x: wp.array,
moduli_y: wp.array,
moduli_z: wp.array,
alpha: wp.array,
volume: wp.array,
grad_mesh_out: wp.array,
grad_grad_convolved: wp.array,
grad_k_squared_out: wp.array,
grad_alpha_out: wp.array,
grad_volume_out: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Single-system double-backward for ``pme_convolve``.
Emits position-relevant second-order terms and cell/stress second-order terms.
Parameters
----------
h_grad_mesh : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
Incoming cotangent for the ``grad_mesh_fft`` output of the backward pass.
h_alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_alpha`` output of the backward pass.
h_volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_volume`` output of the backward pass.
h_grad_ksq : wp.array, shape (nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_k_squared`` output of the backward pass.
mesh_fft : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
Forward mesh values saved from the original forward pass.
grad_convolved : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
Cotangent of the convolved mesh from the backward pass.
k_squared : wp.array, shape (nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Squared k-vector magnitudes from the forward pass.
moduli_x : wp.array, shape (nx,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along x.
moduli_y : wp.array, shape (ny,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along y.
moduli_z : wp.array, shape (nz_rfft,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along z.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter from the forward pass.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume from the forward pass.
grad_mesh_out : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Second-order gradient w.r.t. ``mesh_fft`` (dL/dmesh_fft).
grad_grad_convolved : wp.array, shape (nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Second-order gradient w.r.t. ``grad_convolved`` (dL/dgrad_convolved).
grad_k_squared_out : wp.array, shape (nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Per-k cell/stress second-order gradient (dL/ds per k-point).
Must be zero-initialized before launch.
grad_alpha_out : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``alpha``. Must be zero-initialized;
accumulated atomically over k-points.
grad_volume_out : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``volume``. Must be zero-initialized;
accumulated atomically over k-points.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_convolve_backward` : First-order backward pass.
"""
nx, ny, nz_rfft = mesh_fft.shape[0], mesh_fft.shape[1], mesh_fft.shape[2]
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_convolve", order="double_backward"
)
wp.launch(
kernel,
dim=(nx, ny, nz_rfft),
inputs=[
h_grad_mesh,
h_alpha,
h_volume,
h_grad_ksq,
mesh_fft,
grad_convolved,
k_squared,
moduli_x,
moduli_y,
moduli_z,
alpha,
volume,
],
outputs=[
grad_mesh_out,
grad_grad_convolved,
grad_k_squared_out,
grad_alpha_out,
grad_volume_out,
],
device=device,
)
def batch_pme_convolve_double_backward(
h_grad_mesh: wp.array,
h_alpha: wp.array,
h_volume: wp.array,
h_grad_ksq: wp.array,
mesh_fft: wp.array,
grad_convolved: wp.array,
k_squared: wp.array,
moduli_x: wp.array,
moduli_y: wp.array,
moduli_z: wp.array,
alpha: wp.array,
volume: wp.array,
grad_mesh_out: wp.array,
grad_grad_convolved: wp.array,
grad_k_squared_out: wp.array,
grad_alpha_out: wp.array,
grad_volume_out: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched double-backward for ``batch_pme_convolve``.
Parameters
----------
h_grad_mesh : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
Incoming cotangent for the ``grad_mesh_fft`` output of the backward pass.
h_alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Incoming cotangent for the per-system ``grad_alpha`` output of the backward pass.
h_volume : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Incoming cotangent for the per-system ``grad_volume`` output of the backward pass.
h_grad_ksq : wp.array, shape (B, nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_k_squared`` output of the backward pass.
mesh_fft : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
Per-system forward mesh values saved from the original forward pass.
grad_convolved : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
Per-system cotangent of the convolved mesh from the backward pass.
k_squared : wp.array, shape (B, nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
Per-system squared k-vector magnitudes from the forward pass.
moduli_x : wp.array, shape (nx,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along x (shared across systems).
moduli_y : wp.array, shape (ny,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along y (shared across systems).
moduli_z : wp.array, shape (nz_rfft,), dtype=wp.float32 or wp.float64
B-spline modulus LUT along z (shared across systems).
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter from the forward pass.
volume : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volumes from the forward pass.
grad_mesh_out : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Second-order gradient w.r.t. ``mesh_fft``.
grad_grad_convolved : wp.array, shape (B, nx, ny, nz_rfft), dtype=vec2f or vec2d
OUTPUT: Second-order gradient w.r.t. ``grad_convolved``.
grad_k_squared_out : wp.array, shape (B, nx, ny, nz_rfft), dtype=wp.float32 or wp.float64
OUTPUT: Per-k cell/stress second-order gradient. Must be zero-initialized.
grad_alpha_out : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system second-order gradient w.r.t. ``alpha``. Must be
zero-initialized; accumulated atomically over k-points.
grad_volume_out : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system second-order gradient w.r.t. ``volume``. Must be
zero-initialized; accumulated atomically over k-points.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.batch_pme_convolve_backward` : First-order backward pass.
"""
num_systems = mesh_fft.shape[0]
nx, ny, nz_rfft = mesh_fft.shape[1], mesh_fft.shape[2], mesh_fft.shape[3]
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_convolve", batched=True, order="double_backward"
)
wp.launch(
kernel,
dim=(num_systems, nx, ny, nz_rfft),
inputs=[
h_grad_mesh,
h_alpha,
h_volume,
h_grad_ksq,
mesh_fft,
grad_convolved,
k_squared,
moduli_x,
moduli_y,
moduli_z,
alpha,
volume,
],
outputs=[
grad_mesh_out,
grad_grad_convolved,
grad_k_squared_out,
grad_alpha_out,
grad_volume_out,
],
device=device,
)
[docs]
def pme_energy_corrections(
raw_energies: wp.array,
charges: wp.array,
volume: wp.array,
alpha: wp.array,
total_charge: wp.array,
corrected_energies: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
r"""Apply self-energy and background corrections to PME energies.
Framework-agnostic launcher for single-system energy corrections.
Converts raw potential values :math:`\phi_i` to corrected per-atom energies by:
1. Multiplying potential by charge: :math:`E_{pot} = q_i \cdot \phi_i`
2. Subtracting self-energy: :math:`E_{self} = (\alpha/\sqrt{\pi}) \cdot q_i^2`
3. Subtracting background: :math:`E_{bg} = (\pi/(2\alpha^2 V)) \cdot q_i \cdot Q_{total}`
Final:
.. math::
E_i = q_i \cdot \phi_i - \frac{\alpha}{\sqrt{\pi}} q_i^2 - \frac{\pi}{2\alpha^2 V} q_i Q_{total}
Parameters
----------
raw_energies : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\phi_i` from mesh interpolation.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter.
total_charge : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Sum of all charges (:math:`Q_{total} = \sum_i q_i`).
corrected_energies : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Corrected per-atom energies.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = raw_energies.shape[0]
launch_device = device if device is not None else str(raw_energies.device)
sentinels = _get_pme_factory_sentinels(wp_dtype, launch_device)
kernel = _get_pme_factory_kernel(wp_dtype, component="pme_corrections")
wp.launch(
kernel,
dim=num_atoms,
inputs=[
raw_energies,
charges,
sentinels["batch_idx"],
volume,
alpha,
total_charge,
],
outputs=[corrected_energies, sentinels["atoms"]],
device=launch_device,
)
[docs]
def batch_pme_energy_corrections(
raw_energies: wp.array,
charges: wp.array,
batch_idx: wp.array,
volumes: wp.array,
alpha: wp.array,
total_charges: wp.array,
corrected_energies: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
r"""Apply self-energy and background corrections for batched PME.
Framework-agnostic launcher for batched energy corrections.
Each atom looks up its system's parameters via batch_idx.
Parameters
----------
raw_energies : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\phi_i` from mesh interpolation.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges for all systems concatenated.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volume.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter.
total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system sum of charges (:math:`Q_s = \sum_{i \in s} q_i`).
corrected_energies : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Corrected per-atom energies.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = raw_energies.shape[0]
launch_device = device if device is not None else str(raw_energies.device)
sentinels = _get_pme_factory_sentinels(wp_dtype, launch_device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", batched=True
)
wp.launch(
kernel,
dim=num_atoms,
inputs=[raw_energies, charges, batch_idx, volumes, alpha, total_charges],
outputs=[corrected_energies, sentinels["atoms"]],
device=launch_device,
)
def pme_energy_corrections_backward(
grad_E: wp.array,
raw_energies: wp.array,
charges: wp.array,
volume: wp.array,
alpha: wp.array,
total_charge: wp.array,
grad_raw: wp.array,
grad_charges: wp.array,
grad_volume: wp.array,
grad_alpha: wp.array,
grad_total_charge: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Single-system launcher for factory-backed PME correction backward.
Parameters
----------
grad_E : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Cotangent of the corrected per-atom energies.
raw_energies : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\\phi_i` saved from the forward pass.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges saved from the forward pass.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume saved from the forward pass.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter saved from the forward pass.
total_charge : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Sum of all charges saved from the forward pass.
grad_raw : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``raw_energies``.
grad_charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``charges``.
grad_volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``volume``. Must be zero-initialized.
grad_alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``alpha``. Must be zero-initialized.
grad_total_charge : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``total_charge``. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_energy_corrections` : Corresponding forward pass.
"""
launch_device = device if device is not None else str(raw_energies.device)
sentinels = _get_pme_factory_sentinels(wp_dtype, launch_device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", order="backward"
)
wp.launch(
kernel,
dim=raw_energies.shape[0],
inputs=[
grad_E,
raw_energies,
charges,
sentinels["batch_idx"],
volume,
alpha,
total_charge,
],
outputs=[
grad_raw,
grad_charges,
grad_volume,
grad_alpha,
grad_total_charge,
],
device=launch_device,
)
def batch_pme_energy_corrections_backward(
grad_E: wp.array,
raw_energies: wp.array,
charges: wp.array,
batch_idx: wp.array,
volumes: wp.array,
alpha: wp.array,
total_charges: wp.array,
grad_raw: wp.array,
grad_charges: wp.array,
grad_volumes: wp.array,
grad_alpha: wp.array,
grad_total_charges: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched launcher for factory-backed PME correction backward.
Parameters
----------
grad_E : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Cotangent of the corrected per-atom energies.
raw_energies : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\\phi_i` saved from the forward pass.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges for all systems concatenated, saved from the forward pass.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volumes saved from the forward pass.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter saved from the forward pass.
total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system sum of charges saved from the forward pass.
grad_raw : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``raw_energies``.
grad_charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Gradient w.r.t. ``charges``.
grad_volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``volumes``. Must be zero-initialized.
grad_alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``alpha``. Must be zero-initialized.
grad_total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``total_charges``. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.batch_pme_energy_corrections` : Corresponding forward pass.
"""
launch_device = device if device is not None else str(raw_energies.device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", batched=True, order="backward"
)
wp.launch(
kernel,
dim=raw_energies.shape[0],
inputs=[
grad_E,
raw_energies,
charges,
batch_idx,
volumes,
alpha,
total_charges,
],
outputs=[
grad_raw,
grad_charges,
grad_volumes,
grad_alpha,
grad_total_charges,
],
device=launch_device,
)
def pme_energy_corrections_double_backward(
h_raw: wp.array,
h_chg: wp.array,
h_vol: wp.array,
h_alpha: wp.array,
h_qtot: wp.array,
grad_E: wp.array,
raw_energies: wp.array,
charges: wp.array,
volume: wp.array,
alpha: wp.array,
total_charge: wp.array,
grad_grad_E: wp.array,
grad_raw: wp.array,
grad_charges: wp.array,
grad_volume: wp.array,
grad_alpha: wp.array,
grad_total_charge: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Single-system launcher for factory-backed PME correction double-backward.
Parameters
----------
h_raw : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_raw`` output of the backward pass.
h_chg : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_charges`` output of the backward pass.
h_vol : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_volume`` output of the backward pass.
h_alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_alpha`` output of the backward pass.
h_qtot : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_total_charge`` output of the backward pass.
grad_E : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Cotangent of the corrected per-atom energies from the backward pass.
raw_energies : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\\phi_i` saved from the original forward pass.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges saved from the original forward pass.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume saved from the original forward pass.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter saved from the original forward pass.
total_charge : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Sum of all charges saved from the original forward pass.
grad_grad_E : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``grad_E``.
grad_raw : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``raw_energies``.
grad_charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``charges``.
grad_volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``volume``. Must be zero-initialized.
grad_alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``alpha``. Must be zero-initialized.
grad_total_charge : wp.array, shape (1,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``total_charge``. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_energy_corrections_backward` : First-order backward pass.
"""
launch_device = device if device is not None else str(raw_energies.device)
sentinels = _get_pme_factory_sentinels(wp_dtype, launch_device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", order="double_backward"
)
wp.launch(
kernel,
dim=raw_energies.shape[0],
inputs=[
h_raw,
h_chg,
h_vol,
h_alpha,
h_qtot,
grad_E,
raw_energies,
charges,
sentinels["batch_idx"],
volume,
alpha,
total_charge,
],
outputs=[
grad_grad_E,
grad_raw,
grad_charges,
grad_volume,
grad_alpha,
grad_total_charge,
],
device=launch_device,
)
def batch_pme_energy_corrections_double_backward(
h_raw: wp.array,
h_chg: wp.array,
h_vol: wp.array,
h_alpha: wp.array,
h_qtot: wp.array,
grad_E: wp.array,
raw_energies: wp.array,
charges: wp.array,
batch_idx: wp.array,
volumes: wp.array,
alpha: wp.array,
total_charges: wp.array,
grad_grad_E: wp.array,
grad_raw: wp.array,
grad_charges: wp.array,
grad_volumes: wp.array,
grad_alpha: wp.array,
grad_total_charges: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched launcher for factory-backed PME correction double-backward.
Parameters
----------
h_raw : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_raw`` output of the backward pass.
h_chg : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_charges`` output of the backward pass.
h_vol : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_volumes`` output of the backward pass.
h_alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_alpha`` output of the backward pass.
h_qtot : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Incoming cotangent for the ``grad_total_charges`` output of the backward pass.
grad_E : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Cotangent of the corrected per-atom energies from the backward pass.
raw_energies : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\\phi_i` saved from the original forward pass.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges for all systems concatenated, saved from the original forward pass.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volumes saved from the original forward pass.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter saved from the original forward pass.
total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system sum of charges saved from the original forward pass.
grad_grad_E : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``grad_E``.
grad_raw : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``raw_energies``.
grad_charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Second-order gradient w.r.t. ``charges``.
grad_volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system second-order gradient w.r.t. ``volumes``. Must be zero-initialized.
grad_alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system second-order gradient w.r.t. ``alpha``. Must be zero-initialized.
grad_total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system second-order gradient w.r.t. ``total_charges``. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.batch_pme_energy_corrections_backward` : First-order backward pass.
"""
launch_device = device if device is not None else str(raw_energies.device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", batched=True, order="double_backward"
)
wp.launch(
kernel,
dim=raw_energies.shape[0],
inputs=[
h_raw,
h_chg,
h_vol,
h_alpha,
h_qtot,
grad_E,
raw_energies,
charges,
batch_idx,
volumes,
alpha,
total_charges,
],
outputs=[
grad_grad_E,
grad_raw,
grad_charges,
grad_volumes,
grad_alpha,
grad_total_charges,
],
device=launch_device,
)
[docs]
def pme_energy_corrections_with_charge_grad(
raw_energies: wp.array,
charges: wp.array,
volume: wp.array,
alpha: wp.array,
total_charge: wp.array,
corrected_energies: wp.array,
charge_gradients: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
r"""Apply corrections and compute charge gradients for PME energies.
Framework-agnostic launcher for single-system energy corrections
with analytical charge gradient computation.
Computes both corrected energies and analytical charge gradients:
- Energy: :math:`E_i = q_i \phi_i - (\alpha/\sqrt{\pi}) q_i^2 - (\pi/(2\alpha^2 V)) q_i Q_{total}`
- Charge gradient: :math:`\partial E_{total}/\partial q_i = 2\phi_i - 2(\alpha/\sqrt{\pi})q_i - (\pi/(\alpha^2 V))Q_{total}`
Parameters
----------
raw_energies : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\phi_i` from mesh interpolation.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges.
volume : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Unit cell volume.
alpha : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Ewald splitting parameter.
total_charge : wp.array, shape (1,), dtype=wp.float32 or wp.float64
Sum of all charges (:math:`Q_{total} = \sum_i q_i`).
corrected_energies : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Corrected per-atom energies.
charge_gradients : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Analytical charge gradients :math:`\partial E_{total}/\partial q_i`.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = raw_energies.shape[0]
launch_device = device if device is not None else str(raw_energies.device)
sentinels = _get_pme_factory_sentinels(wp_dtype, launch_device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", charge_grad=True
)
wp.launch(
kernel,
dim=num_atoms,
inputs=[
raw_energies,
charges,
sentinels["batch_idx"],
volume,
alpha,
total_charge,
],
outputs=[corrected_energies, charge_gradients],
device=launch_device,
)
[docs]
def batch_pme_energy_corrections_with_charge_grad(
raw_energies: wp.array,
charges: wp.array,
batch_idx: wp.array,
volumes: wp.array,
alpha: wp.array,
total_charges: wp.array,
corrected_energies: wp.array,
charge_gradients: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
r"""Apply corrections and compute charge gradients for batched PME.
Framework-agnostic launcher for batched energy corrections
with analytical charge gradient computation.
Parameters
----------
raw_energies : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Raw potential values :math:`\phi_i` from mesh interpolation.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges for all systems concatenated.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
volumes : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system unit cell volume.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter.
total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system sum of charges (:math:`Q_s = \sum_{i \in s} q_i`).
corrected_energies : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Corrected per-atom energies.
charge_gradients : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Analytical charge gradients :math:`\partial E_{total}/\partial q_i`.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = raw_energies.shape[0]
launch_device = device if device is not None else str(raw_energies.device)
kernel = _get_pme_factory_kernel(
wp_dtype, component="pme_corrections", batched=True, charge_grad=True
)
wp.launch(
kernel,
dim=num_atoms,
inputs=[raw_energies, charges, batch_idx, volumes, alpha, total_charges],
outputs=[corrected_energies, charge_gradients],
device=launch_device,
)
def pme_virial_bg_correction(
charges: wp.array,
batch_idx: wp.array,
cell: wp.array,
volume: wp.array,
use_supplied_volume: bool,
alpha: wp.array,
total_charges: wp.array,
virial_in: wp.array,
virial_out: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
r"""Apply non-neutral background virial correction.
Two-pass launcher: pass 1 scatter-adds per-atom ``charges`` into per-system
``total_charges`` via atomic_add; pass 2 computes
:math:`E_{bg} = \pi Q^2 / (2 \alpha^2 V)` and subtracts it from the three
diagonal entries of ``virial_in``, writing the result to ``virial_out``.
For a single system, fill ``batch_idx`` with zeros.
Parameters
----------
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Per-atom charges.
batch_idx : wp.array, shape (N,), dtype=wp.int32
System index for each atom (0 to B-1).
cell : wp.array, shape (B, 3, 3), dtype=wp.float32 or wp.float64
Per-system unit cell matrix; volume computed as ``|det(cell[s])|``
when ``use_supplied_volume`` is False.
volume : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Caller-supplied per-system volume. Used only when ``use_supplied_volume``
is True; otherwise a dummy array is acceptable.
use_supplied_volume : bool
If True, use values from ``volume`` rather than computing from ``cell``.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter.
total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system total charge accumulator. Must be zero-initialized by the caller;
written in pass 1 via atomic_add.
virial_in : wp.array, shape (B, 3, 3), dtype=wp.float32 or wp.float64
Input virial tensor per system.
virial_out : wp.array, shape (B, 3, 3), dtype=wp.float32 or wp.float64
OUTPUT: ``virial_in`` with :math:`E_{bg}` subtracted from diagonal entries.
May alias ``virial_in`` for in-place operation.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_virial_bg_correction_backward` : Backward pass.
"""
num_atoms = charges.shape[0]
num_systems = total_charges.shape[0]
wp.launch(
_pme_virial_bg_reduce_kernel_overload[wp_dtype],
dim=num_atoms,
inputs=[charges, batch_idx, total_charges],
device=device,
)
wp.launch(
_pme_virial_bg_apply_kernel_overload[wp_dtype],
dim=num_systems,
inputs=[
total_charges,
cell,
volume,
int(use_supplied_volume),
alpha,
virial_in,
virial_out,
],
device=device,
)
def pme_virial_bg_correction_backward(
grad_virial: wp.array,
charges: wp.array,
batch_idx: wp.array,
cell: wp.array,
volume: wp.array,
use_supplied_volume: bool,
alpha: wp.array,
total_charges: wp.array,
grad_total_charges: wp.array,
grad_charges: wp.array,
grad_alpha: wp.array,
grad_cell: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Analytic backward for ``pme_virial_bg_correction``.
Three-pass launcher:
1. Reduce per-atom ``charges`` into ``total_charges`` (Q per system).
2. Per-system: turn cotangent ``grad_virial`` into ``grad_total_charges``,
``grad_alpha``, and ``grad_cell`` via :math:`dE_{bg}/dQ`, :math:`dE_{bg}/d\\alpha`,
and Jacobi's formula for :math:`d|\\det C|/dC`.
3. Per-atom: scatter ``grad_total_charges[s(j)]`` to ``grad_charges[j]``.
Parameters
----------
grad_virial : wp.array, shape (B, 3, 3), dtype=wp.float32 or wp.float64
Cotangent of the ``virial_out`` output.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Per-atom charges (same as forward pass).
batch_idx : wp.array, shape (N,), dtype=wp.int32
System index for each atom (0 to B-1).
cell : wp.array, shape (B, 3, 3), dtype=wp.float32 or wp.float64
Per-system unit cell matrix from the forward pass.
volume : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Caller-supplied per-system volume. Used only when ``use_supplied_volume`` is True.
use_supplied_volume : bool
If True, use values from ``volume`` rather than computing from ``cell``.
alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system Ewald splitting parameter from the forward pass.
total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
Per-system total charge accumulator. Must be zero-initialized; written in pass 1.
grad_total_charges : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. total charge. Must be zero-initialized.
grad_charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Per-atom gradient w.r.t. ``charges``. Must be zero-initialized.
grad_alpha : wp.array, shape (B,), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``alpha``. Must be zero-initialized.
grad_cell : wp.array, shape (B, 3, 3), dtype=wp.float32 or wp.float64
OUTPUT: Per-system gradient w.r.t. ``cell``. Must be zero-initialized.
Zero when ``use_supplied_volume`` is True.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str or None, optional
Warp device string. If None, inferred from arrays.
See Also
--------
:func:`nvalchemiops.interactions.electrostatics.pme_kernels.pme_virial_bg_correction` : Corresponding forward pass.
"""
num_atoms = charges.shape[0]
num_systems = total_charges.shape[0]
wp.launch(
_pme_virial_bg_reduce_kernel_overload[wp_dtype],
dim=num_atoms,
inputs=[charges, batch_idx, total_charges],
device=device,
)
wp.launch(
_pme_virial_bg_backward_per_system_kernel_overload[wp_dtype],
dim=num_systems,
inputs=[
grad_virial,
total_charges,
cell,
volume,
int(use_supplied_volume),
alpha,
grad_total_charges,
grad_alpha,
grad_cell,
],
device=device,
)
wp.launch(
_pme_virial_bg_backward_per_atom_kernel_overload[wp_dtype],
dim=num_atoms,
inputs=[batch_idx, grad_total_charges, grad_charges],
device=device,
)
###########################################################################################
########################### Module Exports #################################################
###########################################################################################
__all__ = [
# Kernel overloads
"_pme_green_structure_factor_kernel_overload",
"_batch_pme_green_structure_factor_kernel_overload",
# Warp launchers
"pme_green_structure_factor",
"batch_pme_green_structure_factor",
"pme_energy_corrections",
"batch_pme_energy_corrections",
"pme_energy_corrections_backward",
"batch_pme_energy_corrections_backward",
"pme_energy_corrections_double_backward",
"batch_pme_energy_corrections_double_backward",
"pme_energy_corrections_with_charge_grad",
"batch_pme_energy_corrections_with_charge_grad",
"pme_virial_bg_correction",
"pme_virial_bg_correction_backward",
]