# 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.
"""
B-Spline Interpolation Kernels (Pure Warp)
==========================================
This module provides pure Warp kernels and launchers for B-spline interpolation
functions used in mesh-based calculations (e.g., Particle Mesh Ewald).
This module is framework-agnostic - it contains only Warp kernels and launchers.
For PyTorch bindings, use ``nvalchemiops.torch.spline`` instead.
SUPPORTED ORDERS
================
- Order 1: Constant (Nearest Grid Point)
- Order 2: Linear
- Order 3: Quadratic
- Order 4: Cubic (recommended for PME)
- Order 5: Quartic
- Order 6: Quintic
OPERATIONS
==========
1. SPREAD: Scatter atom values to mesh grid
mesh[g] += value[atom] * weight(atom, g)
2. GATHER: Collect mesh values at atom positions
value[atom] = sum_g mesh[g] * weight(atom, g)
3. GATHER_VEC3: Collect 3D vector field values at atom positions
vector[atom] = sum_g mesh[g] * weight(atom, g)
4. GATHER_GRADIENT: Collect mesh values with weight gradients (forces)
grad[atom] = sum_g mesh[g] * grad_weight(atom, g)
5. SPREAD_CHANNELS: Scatter multi-channel values (e.g., multipoles) to mesh
mesh[c, g] += values[atom, c] * weight(atom, g)
6. GATHER_CHANNELS: Collect multi-channel values from mesh
values[atom, c] = sum_g mesh[c, g] * weight(atom, g)
REFERENCES
==========
- Essmann et al. (1995). J. Chem. Phys. 103, 8577 (PME B-splines)
"""
from __future__ import annotations
from typing import Any
import warp as wp
# Disable warp's automatic adjoint (backward) codegen for every kernel in
# this module. All callers route through hand-written backward chains:
# torch via register_warp_op_chain + register_autograd, JAX via
# warp.jax_experimental.jax_kernel(..., enable_backward=False).
wp.set_module_options({"enable_backward": False})
###########################################################################################
########################### B-Spline Weight Functions #####################################
###########################################################################################
@wp.func
def bspline_weight(u: Any, order: wp.int32) -> Any:
"""Compute B-spline basis function M_n(u).
Parameters
----------
u : float (Any)
Parameter in [0, order). Type-generic (float32 or float64).
order : wp.int32
Spline order (1=constant, 2=linear, 3=quadratic, 4=cubic,
5=quartic, 6=quintic).
Returns
-------
float (Any)
Weight value M_n(u). Same type as input.
"""
# Type-generic constants
zero = type(u)(0.0)
one = type(u)(1.0)
two = type(u)(2.0)
three = type(u)(3.0)
four = type(u)(4.0)
six = type(u)(6.0)
if order == 6:
# Quintic cardinal B-spline (degree 5), 6 pieces on [0, 6).
# Coefficients derived from M_6(u) = (1/120) Σ_{j=0..k} (-1)^j C(6,j) (u-j)^5.
twenty_four = type(u)(24.0)
one_twenty = type(u)(120.0)
five = type(u)(5.0)
if u >= zero and u < one:
u2 = u * u
return (u2 * u2 * u) / one_twenty
elif u >= one and u < two:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
u5 = u4 * u
return (
six
- type(u)(30.0) * u
+ type(u)(60.0) * u2
- type(u)(60.0) * u3
+ type(u)(30.0) * u4
- five * u5
) / one_twenty
elif u >= two and u < three:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
u5 = u4 * u
return (
type(u)(-474.0)
+ type(u)(1170.0) * u
- type(u)(1140.0) * u2
+ type(u)(540.0) * u3
- type(u)(120.0) * u4
+ type(u)(10.0) * u5
) / one_twenty
elif u >= three and u < four:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
u5 = u4 * u
return (
type(u)(4386.0)
- type(u)(6930.0) * u
+ type(u)(4260.0) * u2
- type(u)(1260.0) * u3
+ type(u)(180.0) * u4
- type(u)(10.0) * u5
) / one_twenty
elif u >= four and u < five:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
u5 = u4 * u
return (
type(u)(-10974.0)
+ type(u)(12270.0) * u
- type(u)(5340.0) * u2
+ type(u)(1140.0) * u3
- type(u)(120.0) * u4
+ five * u5
) / one_twenty
elif u >= five and u < type(u)(6.0):
v = type(u)(6.0) - u
v2 = v * v
return (v2 * v2 * v) / one_twenty
else:
_ = twenty_four # unused; declared for symmetry
return zero
elif order == 5:
# Quartic cardinal B-spline (degree 4), 5 pieces on [0, 5).
# Coefficients from M_5(u) = (1/24) Σ_{j=0..k} (-1)^j C(5,j) (u-j)^4.
twenty_four = type(u)(24.0)
five = type(u)(5.0)
if u >= zero and u < one:
u2 = u * u
return (u2 * u2) / twenty_four
elif u >= one and u < two:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(-5.0)
+ type(u)(20.0) * u
- type(u)(30.0) * u2
+ type(u)(20.0) * u3
- type(u)(4.0) * u4
) / twenty_four
elif u >= two and u < three:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(155.0)
- type(u)(300.0) * u
+ type(u)(210.0) * u2
- type(u)(60.0) * u3
+ type(u)(6.0) * u4
) / twenty_four
elif u >= three and u < four:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(-655.0)
+ type(u)(780.0) * u
- type(u)(330.0) * u2
+ type(u)(60.0) * u3
- type(u)(4.0) * u4
) / twenty_four
elif u >= four and u < five:
v = five - u
v2 = v * v
return (v2 * v2) / twenty_four
else:
return zero
elif order == 4:
if u >= zero and u < one:
return u * u * u / six
elif u >= one and u < two:
u2 = u * u
u3 = u2 * u
return (
type(u)(-3.0) * u3 + type(u)(12.0) * u2 - type(u)(12.0) * u + four
) / six
elif u >= two and u < three:
u2 = u * u
u3 = u2 * u
return (
three * u3 - type(u)(24.0) * u2 + type(u)(60.0) * u - type(u)(44.0)
) / six
elif u >= three and u < four:
v = four - u
return v * v * v / six
else:
return zero
elif order == 3:
if u >= zero and u < one:
return u * u / two
elif u >= one and u < two:
return type(u)(0.75) - (u - type(u)(1.5)) * (u - type(u)(1.5))
elif u >= two and u < three:
v = three - u
return v * v / two
else:
return zero
elif order == 2:
if u >= zero and u < one:
return u
elif u >= one and u < two:
return two - u
else:
return zero
elif order == 1:
if u >= zero and u < one:
return one
else:
return zero
else:
return zero
@wp.func
def bspline_derivative(u: Any, order: wp.int32) -> Any:
"""Compute B-spline derivative dM_n(u)/du.
Parameters
----------
u : float (Any)
Parameter in [0, order). Type-generic (float32 or float64).
order : wp.int32
Spline order.
Returns
-------
float (Any)
Derivative value. Same type as input.
"""
# Type-generic constants
zero = type(u)(0.0)
one = type(u)(1.0)
two = type(u)(2.0)
three = type(u)(3.0)
four = type(u)(4.0)
six = type(u)(6.0)
if order == 6:
# Derivatives of the quintic pieces from bspline_weight (order 6).
one_twenty = type(u)(120.0)
five = type(u)(5.0)
if u >= zero and u < one:
u2 = u * u
return (five * u2 * u2) / one_twenty
elif u >= one and u < two:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(-30.0)
+ type(u)(120.0) * u
- type(u)(180.0) * u2
+ type(u)(120.0) * u3
- type(u)(25.0) * u4
) / one_twenty
elif u >= two and u < three:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(1170.0)
- type(u)(2280.0) * u
+ type(u)(1620.0) * u2
- type(u)(480.0) * u3
+ type(u)(50.0) * u4
) / one_twenty
elif u >= three and u < four:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(-6930.0)
+ type(u)(8520.0) * u
- type(u)(3780.0) * u2
+ type(u)(720.0) * u3
- type(u)(50.0) * u4
) / one_twenty
elif u >= four and u < five:
u2 = u * u
u3 = u2 * u
u4 = u2 * u2
return (
type(u)(12270.0)
- type(u)(10680.0) * u
+ type(u)(3420.0) * u2
- type(u)(480.0) * u3
+ type(u)(25.0) * u4
) / one_twenty
elif u >= five and u < type(u)(6.0):
# M_6(u) = (6-u)^5 / 120 → M_6'(u) = -5(6-u)^4 / 120 = -(6-u)^4 / 24
v = type(u)(6.0) - u
v2 = v * v
return -(v2 * v2) / type(u)(24.0)
else:
return zero
elif order == 5:
# Derivatives of the quartic pieces from bspline_weight (order 5).
twenty_four = type(u)(24.0)
five = type(u)(5.0)
if u >= zero and u < one:
return (four * u * u * u) / twenty_four
elif u >= one and u < two:
u2 = u * u
u3 = u2 * u
return (
type(u)(20.0)
- type(u)(60.0) * u
+ type(u)(60.0) * u2
- type(u)(16.0) * u3
) / twenty_four
elif u >= two and u < three:
u2 = u * u
u3 = u2 * u
return (
type(u)(-300.0)
+ type(u)(420.0) * u
- type(u)(180.0) * u2
+ type(u)(24.0) * u3
) / twenty_four
elif u >= three and u < four:
u2 = u * u
u3 = u2 * u
return (
type(u)(780.0)
- type(u)(660.0) * u
+ type(u)(180.0) * u2
- type(u)(16.0) * u3
) / twenty_four
elif u >= four and u < five:
# M_5(u) = (5-u)^4 / 24 → M_5'(u) = -4(5-u)^3 / 24 = -(5-u)^3 / 6
v = five - u
return -(v * v * v) / six
else:
return zero
elif order == 4:
if u >= zero and u < one:
return u * u / two
elif u >= one and u < two:
return (type(u)(-9.0) * u * u + type(u)(24.0) * u - type(u)(12.0)) / six
elif u >= two and u < three:
return (type(u)(9.0) * u * u - type(u)(48.0) * u + type(u)(60.0)) / six
elif u >= three and u < four:
v = four - u
return -three * v * v / six
else:
return zero
elif order == 3:
if u >= zero and u < one:
return u
elif u >= one and u < two:
return -two * (u - type(u)(1.5))
elif u >= two and u < three:
return -(three - u)
else:
return zero
elif order == 2:
if u >= zero and u < one:
return one
elif u >= one and u < two:
return -one
else:
return zero
else:
return zero
@wp.func
def bspline_second_derivative(u: Any, order: wp.int32) -> Any:
"""Compute B-spline second derivative ``d^2M_n(u)/du^2``.
Mirrors the order-2 through order-6 coverage of ``bspline_derivative`` (order
1 returns zero, matching the first-derivative convention).
Used by the position-Hessian backward of ``_bspline_gather_gradient_kernel``.
"""
zero = type(u)(0.0)
one = type(u)(1.0)
two = type(u)(2.0)
three = type(u)(3.0)
four = type(u)(4.0)
six = type(u)(6.0)
if order == 6:
# Second derivatives of the quintic pieces from bspline_weight (order 6).
one_twenty = type(u)(120.0)
five = type(u)(5.0)
if u >= zero and u < one:
return (type(u)(20.0) * u * u * u) / one_twenty
elif u >= one and u < two:
u2 = u * u
u3 = u2 * u
return (
type(u)(120.0)
- type(u)(360.0) * u
+ type(u)(360.0) * u2
- type(u)(100.0) * u3
) / one_twenty
elif u >= two and u < three:
u2 = u * u
u3 = u2 * u
return (
type(u)(-2280.0)
+ type(u)(3240.0) * u
- type(u)(1440.0) * u2
+ type(u)(200.0) * u3
) / one_twenty
elif u >= three and u < four:
u2 = u * u
u3 = u2 * u
return (
type(u)(8520.0)
- type(u)(7560.0) * u
+ type(u)(2160.0) * u2
- type(u)(200.0) * u3
) / one_twenty
elif u >= four and u < five:
u2 = u * u
u3 = u2 * u
return (
type(u)(-10680.0)
+ type(u)(6840.0) * u
- type(u)(1440.0) * u2
+ type(u)(100.0) * u3
) / one_twenty
elif u >= five and u < type(u)(6.0):
# M_6 = (6-u)^5 / 120 → M_6'' = 20(6-u)^3 / 120 = (6-u)^3 / 6
v = type(u)(6.0) - u
return (v * v * v) / six
else:
return zero
elif order == 5:
# Second derivatives of the quartic pieces from bspline_weight (order 5).
twenty_four = type(u)(24.0)
five = type(u)(5.0)
if u >= zero and u < one:
return (type(u)(12.0) * u * u) / twenty_four
elif u >= one and u < two:
u2 = u * u
return (
type(u)(-60.0) + type(u)(120.0) * u - type(u)(48.0) * u2
) / twenty_four
elif u >= two and u < three:
u2 = u * u
return (
type(u)(420.0) - type(u)(360.0) * u + type(u)(72.0) * u2
) / twenty_four
elif u >= three and u < four:
u2 = u * u
return (
type(u)(-660.0) + type(u)(360.0) * u - type(u)(48.0) * u2
) / twenty_four
elif u >= four and u < five:
# M_5 = (5-u)^4 / 24 → M_5'' = 12(5-u)^2 / 24 = (5-u)^2 / 2
v = five - u
return (v * v) / two
else:
return zero
elif order == 4:
# W(u) over [k, k+1] for k in {0..3} (see bspline_weight for forms).
if u >= zero and u < one:
# W''(u) = u
return u
elif u >= one and u < two:
# W' = (-9u² + 24u - 12)/6 → W'' = -3u + 4
return -three * u + four
elif u >= two and u < three:
# W' = (9u² - 48u + 60)/6 → W'' = 3u - 8
return three * u - type(u)(8.0)
elif u >= three and u < four:
# W = (4-u)³/6 → W'' = 4 - u
return four - u
else:
return zero
elif order == 3:
if u >= zero and u < one:
return one
elif u >= one and u < two:
return -two
elif u >= two and u < three:
return one
else:
return zero
elif order == 2:
# First derivative is piecewise constant, so second derivative is 0.
return zero
else:
return zero
@wp.func
def bspline_weight_hessian_dot_vec3(
theta: Any,
offset: wp.vec3i,
order: wp.int32,
mesh_dims: wp.vec3i,
v: Any,
) -> Any:
"""Compute ``H @ v`` where ``H`` is the scaled 3x3 Hessian of the 3D
B-spline weight at the given stencil point.
The Hessian is symmetric and has entries
``H[c, d] = mesh_dims[c] * d^2W/dtheta_c dtheta_d * mesh_dims[d]`` (matching the
``mesh_dims``-scaling convention used by ``bspline_weight_gradient_3d``).
Off-diagonal entries are products of two 1D first-derivatives; diagonal
entries multiply the 1D second-derivative by the other two 1D weights.
Returning ``H @ v`` directly avoids constructing a generic-dtype
``mat33`` in Warp (which is awkward across float32/float64) and saves
the calling kernel from doing the matrix-vector product separately.
"""
t0 = theta[0]
half_order = type(t0)(order) * type(t0)(0.5)
zero = type(t0)(0.0)
order_f = type(t0)(order)
u_x = half_order + t0 - type(t0)(offset[0])
u_y = half_order + theta[1] - type(t0)(offset[1])
u_z = half_order + theta[2] - type(t0)(offset[2])
if (
u_x < zero
or u_x >= order_f
or u_y < zero
or u_y >= order_f
or u_z < zero
or u_z >= order_f
):
return type(v)(zero, zero, zero)
w_x = bspline_weight(u_x, order)
w_y = bspline_weight(u_y, order)
w_z = bspline_weight(u_z, order)
dw_x = bspline_derivative(u_x, order)
dw_y = bspline_derivative(u_y, order)
dw_z = bspline_derivative(u_z, order)
d2w_x = bspline_second_derivative(u_x, order)
d2w_y = bspline_second_derivative(u_y, order)
d2w_z = bspline_second_derivative(u_z, order)
mx = type(t0)(mesh_dims[0])
my = type(t0)(mesh_dims[1])
mz = type(t0)(mesh_dims[2])
Hxx = d2w_x * w_y * w_z * mx * mx
Hyy = w_x * d2w_y * w_z * my * my
Hzz = w_x * w_y * d2w_z * mz * mz
Hxy = dw_x * dw_y * w_z * mx * my
Hxz = dw_x * w_y * dw_z * mx * mz
Hyz = w_x * dw_y * dw_z * my * mz
return type(v)(
Hxx * v[0] + Hxy * v[1] + Hxz * v[2],
Hxy * v[0] + Hyy * v[1] + Hyz * v[2],
Hxz * v[0] + Hyz * v[1] + Hzz * v[2],
)
###########################################################################################
########################### Grid Utility Functions ########################################
###########################################################################################
@wp.func
def compute_fractional_coords(
position: Any,
cell_inv_t: Any,
mesh_dims: wp.vec3i,
) -> Any:
"""Convert Cartesian position to mesh coordinates.
Parameters
----------
position : vec3 (Any)
Atomic position. Type-generic (vec3f or vec3d).
cell_inv_t : mat33 (Any)
Transpose of inverse cell. Type-generic (mat33f or mat33d).
mesh_dims : wp.vec3i
Mesh dimensions.
Returns
-------
base_grid : wp.vec3i
Base grid point (floor of mesh coords).
theta : vec3 (Any)
Fractional part [0, 1) in each dimension. Same type as position.
Note: Returns (base_grid, theta) as a tuple via multiple return values.
"""
# Convert to fractional coordinates
frac = cell_inv_t * position
p0 = position[0]
# Scale to mesh coordinates
mesh_x = frac[0] * type(p0)(mesh_dims[0])
mesh_y = frac[1] * type(p0)(mesh_dims[1])
mesh_z = frac[2] * type(p0)(mesh_dims[2])
# Base grid point
mx = wp.int32(wp.floor(mesh_x))
my = wp.int32(wp.floor(mesh_y))
mz = wp.int32(wp.floor(mesh_z))
# Fractional part
theta_x = mesh_x - type(p0)(mx)
theta_y = mesh_y - type(p0)(my)
theta_z = mesh_z - type(p0)(mz)
return wp.vec3i(mx, my, mz), type(position)(theta_x, theta_y, theta_z)
@wp.func
def bspline_grid_offset(
point_idx: wp.int32,
order: wp.int32,
theta: Any,
) -> wp.vec3i:
"""Compute grid offset for B-spline point index.
For B-splines, points are indexed 0 to order^3-1 and arranged in a cube.
The offset is computed such that the B-spline parameter u is always in [0, n).
The offset_start for each dimension is floor(theta - (n-2)/2), which ensures
that for any theta in [0, 1), all n grid points have valid u values.
Parameters
----------
point_idx : wp.int32
Linear point index (0 to order^3-1).
order : wp.int32
Spline order.
theta : vec3 (Any)
Fractional position within the base grid cell [0, 1) in each dimension.
Type-generic (vec3f or vec3d).
Returns
-------
wp.vec3i
Grid offset (relative to base grid point).
"""
order2 = order * order
i = point_idx // order2
j = (point_idx % order2) // order
k = point_idx % order
t0 = theta[0]
# Compute offset_start = floor(theta - (n-2)/2) for each dimension
# This ensures u = n/2 + theta - offset is always in [0, n).
# Warp 1.13.0 adjoint-codegen mis-types `type(t0)(order - 2)` so use
# an int variable for the subtraction and cast once at the end.
n_minus_2 = order - 2 # int32, no float involvement
half_n_minus_1 = type(t0)(n_minus_2) * type(t0)(0.5)
offset_start_x = wp.int32(wp.floor(t0 - half_n_minus_1))
offset_start_y = wp.int32(wp.floor(theta[1] - half_n_minus_1))
offset_start_z = wp.int32(wp.floor(theta[2] - half_n_minus_1))
return wp.vec3i(i + offset_start_x, j + offset_start_y, k + offset_start_z)
@wp.func
def bspline_weight_3d(
theta: Any,
offset: wp.vec3i,
order: wp.int32,
) -> Any:
"""Compute 3D B-spline weight (separable product).
The B-spline parameter u is computed as:
.. math::
u = \\text{order}/2 + \\theta - \\text{offset}
When offset = i + offset_start (from bspline_grid_offset), this gives
u values in [0, n) that sum to 1 and are centered at the atom position.
Parameters
----------
theta : vec3 (Any)
Fractional position within the base grid cell [0, 1).
Type-generic (vec3f or vec3d).
offset : wp.vec3i
Grid offset from base grid point (includes offset_start adjustment).
order : wp.int32
Spline order.
Returns
-------
float (Any)
Weight = M(u_x) * M(u_y) * M(u_z). Same scalar type as theta.
"""
# Get scalar type from theta vector
t0 = theta[0]
half_order = type(t0)(order) * type(t0)(0.5)
zero = type(t0)(0.0)
order_f = type(t0)(order)
# u = n/2 + theta - offset
u_x = half_order + t0 - type(t0)(offset[0])
u_y = half_order + theta[1] - type(t0)(offset[1])
u_z = half_order + theta[2] - type(t0)(offset[2])
if (
u_x < zero
or u_x >= order_f
or u_y < zero
or u_y >= order_f
or u_z < zero
or u_z >= order_f
):
return zero
return (
bspline_weight(u_x, order)
* bspline_weight(u_y, order)
* bspline_weight(u_z, order)
)
@wp.func
def bspline_weight_gradient_3d(
theta: Any,
offset: wp.vec3i,
order: wp.int32,
mesh_dims: wp.vec3i,
) -> Any:
r"""Compute gradient of 3D B-spline weight.
The B-spline parameter u is computed as:
.. math::
u = \text{order}/2 + \theta - \text{offset}
The gradient with respect to theta is:
.. math::
\begin{aligned}
\frac{\partial u}{\partial \theta} &= +1 \\
\frac{\partial \text{weight}}{\partial \theta} &= \frac{\partial M}{\partial u} \cdot \frac{\partial u}{\partial \theta} = \frac{\partial M}{\partial u}
\end{aligned}
Parameters
----------
theta : vec3 (Any)
Fractional position within the base grid cell [0, 1).
Type-generic (vec3f or vec3d).
offset : wp.vec3i
Grid offset from base grid point (includes offset_start adjustment).
order : wp.int32
Spline order.
mesh_dims : wp.vec3i
Mesh dimensions (for scaling to Cartesian coordinates).
Returns
-------
vec3 (Any)
Gradient :math:`\nabla` weight in fractional coordinates (scaled by mesh_dims).
Same type as theta.
"""
# Get scalar type from theta vector
t0 = theta[0]
half_order = type(t0)(order) * type(t0)(0.5)
zero = type(t0)(0.0)
order_f = type(t0)(order)
# u = n/2 + theta - offset
u_x = half_order + t0 - type(t0)(offset[0])
u_y = half_order + theta[1] - type(t0)(offset[1])
u_z = half_order + theta[2] - type(t0)(offset[2])
if (
u_x < zero
or u_x >= order_f
or u_y < zero
or u_y >= order_f
or u_z < zero
or u_z >= order_f
):
return type(theta)(zero, zero, zero)
w_x = bspline_weight(u_x, order)
w_y = bspline_weight(u_y, order)
w_z = bspline_weight(u_z, order)
# Positive sign because u = half_order + theta - offset, so ∂u/∂theta = +1
dw_x = bspline_derivative(u_x, order) * type(t0)(mesh_dims[0])
dw_y = bspline_derivative(u_y, order) * type(t0)(mesh_dims[1])
dw_z = bspline_derivative(u_z, order) * type(t0)(mesh_dims[2])
return type(theta)(dw_x * w_y * w_z, w_x * dw_y * w_z, w_x * w_y * dw_z)
@wp.func
def bspline_third_derivative(u: Any, order: wp.int32) -> Any:
r"""Compute B-spline third derivative ``d^3M_n(u)/du^3``.
Same piecewise structure as :func:`bspline_second_derivative`. Used
by the multipole-PME ``l_max = 2`` (quadrupole) backward kernels ---
the position-gradient slot :math:`\partial L/\partial r_i` of the Q channel needs
:math:`\partial^3 B` (since :math:`E_\text{recip}^{(Q)} \propto Q : \nabla^2 \varphi`
and :math:`\partial/\partial r_i` adds one more derivative).
Defined for orders 4, 5, 6 (cubic and above). Lower orders return
zero (their third derivative is a Dirac delta train, sampled at
interior smooth points to be zero).
Parameters
----------
u : float (Any)
Parameter in ``[0, order)``. Type-generic (float32 or float64).
order : wp.int32
Spline order.
Returns
-------
float (Any)
Third-derivative value. Same type as ``u``.
"""
zero = type(u)(0.0)
one = type(u)(1.0)
two = type(u)(2.0)
three = type(u)(3.0)
four = type(u)(4.0)
five = type(u)(5.0)
six = type(u)(6.0)
if order == 6:
# M_6''' is quadratic on each of 6 unit pieces.
u2 = u * u
c120 = type(u)(120.0)
if u >= zero and u < one:
# M_6''(u) = 20 u³/120 → M_6'''(u) = 60 u²/120 = u²/2.
return type(u)(60.0) * u2 / c120
elif u >= one and u < two:
# M_6'' = (-100 u³ + 360 u² − 360 u + 120)/120
# → M_6''' = (-300 u² + 720 u − 360)/120
return (type(u)(-300.0) * u2 + type(u)(720.0) * u + type(u)(-360.0)) / c120
elif u >= two and u < three:
# M_6'' = (200 u³ − 1440 u² + 3240 u − 2280)/120
# → M_6''' = (600 u² − 2880 u + 3240)/120
return (type(u)(600.0) * u2 + type(u)(-2880.0) * u + type(u)(3240.0)) / c120
elif u >= three and u < four:
# M_6'' = (-200 u³ + 2160 u² − 7560 u + 8520)/120
# → M_6''' = (-600 u² + 4320 u − 7560)/120
return (
type(u)(-600.0) * u2 + type(u)(4320.0) * u + type(u)(-7560.0)
) / c120
elif u >= four and u < five:
# M_6'' = (100 u³ − 1440 u² + 6840 u − 10680)/120
# → M_6''' = (300 u² − 2880 u + 6840)/120
return (type(u)(300.0) * u2 + type(u)(-2880.0) * u + type(u)(6840.0)) / c120
elif u >= five and u < six:
# M_6''(u) = 20 (6-u)³/120 → ∂/∂u of that
# = 20 · 3 (6-u)² · (-1) / 120 = -60 (6-u)²/120
v = six - u
return -type(u)(60.0) * v * v / c120
else:
return zero
elif order == 5:
# M_5''' is linear on each of 5 unit pieces.
c24 = type(u)(24.0)
if u >= zero and u < one:
return type(u)(24.0) * u / c24 # ≡ u
elif u >= one and u < two:
return (type(u)(-96.0) * u + type(u)(120.0)) / c24
elif u >= two and u < three:
return (type(u)(144.0) * u + type(u)(-360.0)) / c24
elif u >= three and u < four:
return (type(u)(-96.0) * u + type(u)(360.0)) / c24
elif u >= four and u < five:
# M_5'' = 12 (5-u)²/24 → d/du = -24(5-u)/24 = -(5-u)
v = five - u
return -v
else:
return zero
elif order == 4:
# M_4''' is piecewise constant on each of 4 unit pieces.
if u >= zero and u < one:
# M_4''(u) = u → M_4'''(u) = 1
return one
elif u >= one and u < two:
# M_4''(u) = 4 - 3u → M_4'''(u) = -3
return -three
elif u >= two and u < three:
# M_4''(u) = 3u - 8 → M_4'''(u) = 3
return three
elif u >= three and u < four:
# M_4''(u) = 4 - u → M_4'''(u) = -1
return -one
else:
return zero
else:
# Orders 1, 2, 3 are piecewise constant/linear/quadratic; the
# third derivative is zero on the open interior of each piece.
return zero
@wp.func
def bspline_fourth_derivative(u: Any, order: wp.int32) -> Any:
r"""Compute B-spline fourth derivative ``d^4M_n(u)/du^4``.
Same piecewise structure as :func:`bspline_third_derivative`, one order
higher. Used by the multipole-PME ``l_max = 2`` **double-backward** (Q5r-2):
the position-position Hessian block :math:`\partial(\partial L/\partial r_i)/\partial r_j`
of the Q channel needs :math:`\partial^4 B` (the Q-channel forward spread
already carries :math:`\partial^2 B`, its first backward :math:`\partial^3 B`,
and create_graph adds one more).
Defined for orders 5, 6. ``M_6'''`` is quadratic per piece -- ``M_6''''``
is **linear** per piece (:math:`C^0`-continuous, since ``M_6`` is :math:`C^4`).
``M_5'''`` is linear per piece -- ``M_5''''`` is **piecewise-constant**
``(1, -4, 6, -4, 1)`` (discontinuous at knots -- ``M_5`` is only :math:`C^3`).
Orders <= 4 return zero (their fourth derivative is a Dirac train, zero on the
open interior).
Parameters
----------
u : float (Any)
Parameter in ``[0, order)``. Type-generic (float32 or float64).
order : wp.int32
Spline order.
Returns
-------
float (Any)
Fourth-derivative value. Same type as ``u``.
"""
zero = type(u)(0.0)
one = type(u)(1.0)
two = type(u)(2.0)
three = type(u)(3.0)
four = type(u)(4.0)
five = type(u)(5.0)
six = type(u)(6.0)
if order == 6:
# M_6'''' is linear on each of 6 unit pieces (∂/∂u of M_6''', /120).
c120 = type(u)(120.0)
if u >= zero and u < one:
return type(u)(120.0) * u / c120
elif u >= one and u < two:
return (type(u)(-600.0) * u + type(u)(720.0)) / c120
elif u >= two and u < three:
return (type(u)(1200.0) * u + type(u)(-2880.0)) / c120
elif u >= three and u < four:
return (type(u)(-1200.0) * u + type(u)(4320.0)) / c120
elif u >= four and u < five:
return (type(u)(600.0) * u + type(u)(-2880.0)) / c120
elif u >= five and u < six:
# ∂/∂u of -60(6-u)²/120 = 120(6-u)/120 = (6-u).
return six - u
else:
return zero
elif order == 5:
# M_5'''' is piecewise-constant (1, -4, 6, -4, 1) on the 5 unit pieces.
if u >= zero and u < one:
return one
elif u >= one and u < two:
return -four
elif u >= two and u < three:
return six
elif u >= three and u < four:
return -four
elif u >= four and u < five:
return one
else:
return zero
else:
# Orders <= 4: fourth derivative is zero on the open interior of each
# piece (M_4''' is piecewise constant; lower orders even smoother here).
return zero
@wp.func
def bspline_weight_hessian_3d(
theta: Any,
offset: wp.vec3i,
order: wp.int32,
mesh_dims: wp.vec3i,
):
r"""Compute the Hessian of the 3D B-spline weight at a grid offset.
The 3D weight is the product of three 1D weights:
.. math::
w_{3D}(\theta) = M_n(u_x) \, M_n(u_y) \, M_n(u_z)
where :math:`u_\alpha = \mathrm{order}/2 + \theta_\alpha - \mathrm{offset}_\alpha`.
The Hessian is the symmetric 3x3 matrix of second partials:
* Diagonal :math:`\partial^2 w_{3D}/\partial \theta_\alpha^2 = M_n''(u_\alpha) \cdot \prod_{\beta \neq \alpha} M_n(u_\beta)`.
* Off-diagonal :math:`\partial^2 w_{3D}/\partial \theta_\alpha \partial \theta_\beta = M_n'(u_\alpha) \, M_n'(u_\beta) \, M_n(u_\gamma)`
with :math:`\gamma` the remaining axis.
Each component is scaled by ``mesh_dims_alpha * mesh_dims_beta`` to match
the fractional-mesh convention used by :func:`bspline_weight_gradient_3d`
(the chain rule from parametric ``u`` to ``r`` introduces the same
Jacobian factors per derivative).
Returns two ``vec3`` tiles:
* ``diag = (Hxx, Hyy, Hzz)`` (3-vector of diagonal entries).
* ``off = (Hxy, Hxz, Hyz)`` (3-vector of unique off-diagonal entries).
The full symmetric Hessian is::
H = [[diag.x, off.x, off.y],
[off.x, diag.y, off.z],
[off.y, off.z, diag.z]]
Two-vec3 return chosen over a ``mat33`` to avoid importing matrix
types into the spline-primitive layer; callers (e.g., the
Multipole PME Hessian-gather kernel) accumulate ``mu_i * H * mu_j``
directly from the six unique entries without materializing a
matrix.
Parameters
----------
theta : vec3 (Any)
Fractional position within the base grid cell [0, 1).
Type-generic (vec3f or vec3d).
offset : wp.vec3i
Grid offset from base grid point (includes offset_start adjustment).
order : wp.int32
Spline order.
mesh_dims : wp.vec3i
Mesh dimensions (for scaling to Cartesian coordinates).
Returns
-------
diag : vec3 (Any)
``(Hxx, Hyy, Hzz)`` in fractional mesh-space.
off : vec3 (Any)
``(Hxy, Hxz, Hyz)`` in fractional mesh-space.
"""
t0 = theta[0]
half_order = type(t0)(order) * type(t0)(0.5)
zero = type(t0)(0.0)
order_f = type(t0)(order)
u_x = half_order + t0 - type(t0)(offset[0])
u_y = half_order + theta[1] - type(t0)(offset[1])
u_z = half_order + theta[2] - type(t0)(offset[2])
if (
u_x < zero
or u_x >= order_f
or u_y < zero
or u_y >= order_f
or u_z < zero
or u_z >= order_f
):
zvec = type(theta)(zero, zero, zero)
return zvec, zvec
w_x = bspline_weight(u_x, order)
w_y = bspline_weight(u_y, order)
w_z = bspline_weight(u_z, order)
dw_x = bspline_derivative(u_x, order)
dw_y = bspline_derivative(u_y, order)
dw_z = bspline_derivative(u_z, order)
ddw_x = bspline_second_derivative(u_x, order)
ddw_y = bspline_second_derivative(u_y, order)
ddw_z = bspline_second_derivative(u_z, order)
md_x = type(t0)(mesh_dims[0])
md_y = type(t0)(mesh_dims[1])
md_z = type(t0)(mesh_dims[2])
diag = type(theta)(
ddw_x * w_y * w_z * md_x * md_x,
w_x * ddw_y * w_z * md_y * md_y,
w_x * w_y * ddw_z * md_z * md_z,
)
off = type(theta)(
dw_x * dw_y * w_z * md_x * md_y,
dw_x * w_y * dw_z * md_x * md_z,
w_x * dw_y * dw_z * md_y * md_z,
)
return diag, off
@wp.func
def wrap_grid_index(idx: wp.int32, dim: wp.int32) -> wp.int32:
"""Wrap grid index for periodic boundaries."""
return ((idx % dim) + dim) % dim
###########################################################################################
########################### Single-System Warp Kernels ####################################
###########################################################################################
@wp.kernel
def _bspline_weight_kernel(
u: wp.array(dtype=Any),
order: wp.int32,
weights: wp.array(dtype=Any),
):
"""Compute B-spline weights for an array of inputs.
Parameters
----------
u : wp.array, shape (N,)
Input values.
order : wp.int32
Spline order.
weights : wp.array, shape (N,)
Output weights.
"""
i = wp.tid()
weights[i] = bspline_weight(u[i], order)
@wp.kernel
def _bspline_spread_kernel(
positions: wp.array(dtype=Any),
values: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array3d(dtype=Any),
):
"""Spread (scatter) values from atoms to a 3D mesh using B-spline interpolation.
For each atom, distributes its value to nearby grid points weighted by the
B-spline basis function. This is the adjoint operation to gathering.
Formula: mesh[g] += value[atom] * w(atom, g)
where w(atom, g) is the product of 1D B-spline weights in each dimension.
Launch Grid
-----------
dim = [num_atoms, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates in Cartesian space.
values : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Values to spread (e.g., charges).
cell_inv_t : wp.array, shape (1, 3, 3), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix for fractional coordinate conversion.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array3d, shape (nx, ny, nz), dtype=wp.float32 or wp.float64
OUTPUT: 3D mesh to accumulate values into. Must be zero-initialized.
Notes
-----
- Uses atomic adds for thread-safe accumulation to shared grid points.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight skip the atomic add for efficiency.
- Layout is per-(atom, stencil-point) rather than per-atom to keep broad
parallelism across the stencil. The per-order specialized kernels
(see ``_PER_ORDER_*`` below) take a different approach: full unroll of
the order^3 stencil per atom, used for orders 2-6.
"""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
value = values[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
if weight > type(value)(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
wp.atomic_add(mesh, gx, gy, gz, value * weight)
@wp.kernel
def _bspline_gather_kernel(
positions: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array3d(dtype=Any),
output: wp.array(dtype=Any),
):
"""Gather (interpolate) values from a 3D mesh to atom positions using B-splines.
For each atom, interpolates the mesh value at its position by summing nearby
grid points weighted by the B-spline basis function.
Formula: output[atom] = sum_g mesh[g] * w(atom, g)
where the sum is over the order^3 grid points in the atom's stencil.
Launch Grid
-----------
dim = [num_atoms, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates in Cartesian space.
cell_inv_t : wp.array, shape (1, 3, 3), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix for fractional coordinate conversion.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array3d, shape (nx, ny, nz), dtype=wp.float32 or wp.float64
3D mesh containing values to interpolate (e.g., electrostatic potential).
output : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Interpolated values per atom. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's output.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight skip the atomic add for efficiency.
- The per-(atom, stencil-point) layout keeps neighboring threads on the
same atom stencil. Per-atom register accumulation avoids atom-output
atomics, but changes mesh-read locality and should be benchmarked before
replacing this generic launcher.
"""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
mesh_val = mesh[0, 0, 0] # Get type reference
if weight > type(mesh_val)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[gx, gy, gz]
wp.atomic_add(output, atom_idx, mesh_val * weight)
@wp.kernel
def _bspline_gather_vec3_kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array3d(dtype=Any),
output: wp.array(dtype=Any),
):
"""Gather charge-weighted 3D vector values from mesh to atoms using B-splines.
Similar to _bspline_gather_kernel but multiplies by the atom's charge and
outputs to a 3D vector array (for use with vector-valued mesh fields).
Formula: output[atom] = q[atom] * sum_g mesh[g] * w(atom, g)
Launch Grid
-----------
dim = [num_atoms, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates in Cartesian space.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges (or other scalar weights).
cell_inv_t : wp.array, shape (1, 3, 3), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix for fractional coordinate conversion.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array3d, shape (nx, ny, nz), dtype=wp.vec3f or wp.vec3d
3D mesh containing vector values to interpolate.
output : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Charge-weighted interpolated vectors per atom. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's output.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic add for efficiency.
"""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
if weight > type(charge)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[gx, gy, gz]
wp.atomic_add(output, atom_idx, charge * mesh_val * weight)
@wp.kernel
def _bspline_gather_with_force_kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array3d(dtype=Any),
output: wp.array(dtype=Any),
forces: wp.array(dtype=Any),
):
"""Single-pass interpolation: gather potential AND spline-derivative force.
Reads each mesh stencil cell ONCE, accumulating both:
- ``output[atom] += sum_g mesh[g] * w(atom, g)`` (raw potential)
- ``forces[atom] += -q_atom * sum_g mesh[g] * (Cell^{-T} grad_w)`` (Cartesian force)
This replaces calling ``_bspline_gather_kernel`` followed by
``_bspline_gather_gradient_kernel`` on the same mesh -- they would each
re-read every stencil cell and recompute the per-thread weight
derivatives. The fused kernel halves the mesh DRAM traffic for the
PME-with-forces path and reuses one set of 1D weight evaluations across
both output channels.
Launch Grid
-----------
dim = [num_atoms, order^3]
Parameters
----------
positions, charges, cell_inv_t, order, mesh :
Same as ``_bspline_gather_kernel`` / ``_bspline_gather_gradient_kernel``.
output : wp.array, shape (N,), dtype=float32/float64
OUTPUT: raw potential per atom. Must be zero-initialized.
forces : wp.array, shape (N,), dtype=vec3f/vec3d
OUTPUT: Cartesian force per atom (already including -q). Must be
zero-initialized.
"""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
t0 = theta[0]
half_order = type(t0)(order) * type(t0)(0.5)
zero = type(t0)(0.0)
order_f = type(t0)(order)
u_x = half_order + theta[0] - type(t0)(offset[0])
u_y = half_order + theta[1] - type(t0)(offset[1])
u_z = half_order + theta[2] - type(t0)(offset[2])
if (
u_x < zero
or u_x >= order_f
or u_y < zero
or u_y >= order_f
or u_z < zero
or u_z >= order_f
):
return
# One 1D-weight + derivative evaluation per axis per thread, reused for
# both the scalar potential weight and the three gradient components.
w_x = bspline_weight(u_x, order)
w_y = bspline_weight(u_y, order)
w_z = bspline_weight(u_z, order)
dw_x = bspline_derivative(u_x, order) * type(t0)(mesh_dims[0])
dw_y = bspline_derivative(u_y, order) * type(t0)(mesh_dims[1])
dw_z = bspline_derivative(u_z, order) * type(t0)(mesh_dims[2])
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[gx, gy, gz]
# Scalar potential contribution. The `weight > 1e-8` cutoff matches the
# original ``_bspline_gather_kernel`` so the fused output is byte-identical
# to the (un-fused) two-kernel path; without it, near-zero stencil-edge
# contributions get included and the accumulated sum order changes enough
# to violate the tight momentum-conservation tolerance.
weight = w_x * w_y * w_z
if weight > type(mesh_val)(1e-8):
wp.atomic_add(output, atom_idx, mesh_val * weight)
# Fractional-coordinate gradient → Cartesian force. ``grad_mag > 0`` mirrors
# the original ``_bspline_gather_gradient_kernel``.
grad_x = dw_x * w_y * w_z
grad_y = w_x * dw_y * w_z
grad_z = w_x * w_y * dw_z
grad_mag = wp.abs(grad_x) + wp.abs(grad_y) + wp.abs(grad_z)
if grad_mag > type(charge)(0.0):
force_frac = type(position)(
-charge * mesh_val * grad_x,
-charge * mesh_val * grad_y,
-charge * mesh_val * grad_z,
)
force = wp.transpose(cell_inv_t[0]) * force_frac
wp.atomic_add(forces, atom_idx, force)
###########################################################################################
####### Per-order specialized fused gather — per-atom + register accumulation #############
###########################################################################################
#
# Per-order specialized kernels: ORDER is captured as a Python int literal
# so Warp unrolls the order^3 stencil at codegen time. Each (order, dtype)
# lives in its own named warp module so warp NVRTC-compiles only the
# orders actually launched. enable_backward=False is set per-module.
_PER_ORDER_VEC = {
(2, wp.float32): wp.types.vector(length=2, dtype=wp.float32),
(2, wp.float64): wp.types.vector(length=2, dtype=wp.float64),
(3, wp.float32): wp.types.vector(length=3, dtype=wp.float32),
(3, wp.float64): wp.types.vector(length=3, dtype=wp.float64),
(4, wp.float32): wp.types.vector(length=4, dtype=wp.float32),
(4, wp.float64): wp.types.vector(length=4, dtype=wp.float64),
(5, wp.float32): wp.types.vector(length=5, dtype=wp.float32),
(5, wp.float64): wp.types.vector(length=5, dtype=wp.float64),
(6, wp.float32): wp.types.vector(length=6, dtype=wp.float32),
(6, wp.float64): wp.types.vector(length=6, dtype=wp.float64),
}
def _per_order_module(kind: str, order: int, scalar_dtype) -> wp.Module:
"""Return a named warp module for a (kind, order, dtype) per-order kernel.
Using a distinct warp module per tuple means warp compiles ONLY the
orders/dtypes the user actually launches at runtime. Module options
(``enable_backward=False``) are applied here, isolated from the
parent ``nvalchemiops.math.spline`` module's settings.
"""
dtype_tag = "fp32" if scalar_dtype is wp.float32 else "fp64"
mod = wp.get_module(
f"nvalchemiops.math.spline_per_order.{kind}_order{order}_{dtype_tag}"
)
# The adjoint is never used (see top-of-file note); skipping it saves
# ~70% of generated code for these heavily-unrolled kernels.
mod.options["enable_backward"] = False
return mod
def _make_bspline_gather_with_force_kernel(
ORDER: int,
scalar_dtype,
vec_pos_dtype,
mat33_dtype,
):
"""Factory: per-order specialized fused gather kernel.
Returns a Warp kernel parameterized for the given spline ``ORDER``. The
kernel walks the order^3 stencil entirely in registers -- fully unrolled
by Warp's codegen because ORDER is a Python int literal in scope -- and
writes potential and force without atomics.
"""
vec_ord = _PER_ORDER_VEC[(ORDER, scalar_dtype)]
_vec_pos = vec_pos_dtype
# Pre-compute Python-side float constants so the kernel never has to
# do an int→float cast that Warp 1.13.0's adjoint codegen mishandles.
HALF_ORDER_PY = float(ORDER) * 0.5
HALF_N_MINUS_2_PY = float(ORDER - 2) * 0.5
@wp.kernel(module=_per_order_module("gather_with_force", ORDER, scalar_dtype))
def kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
mesh: wp.array3d(dtype=Any),
output: wp.array(dtype=Any),
forces: wp.array(dtype=Any),
):
atom_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
t0 = theta[0]
half_order = type(t0)(HALF_ORDER_PY)
half_n_minus_2 = type(t0)(HALF_N_MINUS_2_PY)
zero = type(t0)(0.0)
offset_start_x = wp.int32(wp.floor(theta[0] - half_n_minus_2))
offset_start_y = wp.int32(wp.floor(theta[1] - half_n_minus_2))
offset_start_z = wp.int32(wp.floor(theta[2] - half_n_minus_2))
# 1D weights + derivatives per axis (3 * ORDER evaluations total).
wx = vec_ord()
wy = vec_ord()
wz = vec_ord()
dwx = vec_ord()
dwy = vec_ord()
dwz = vec_ord()
for k in range(ORDER):
u_x = half_order + theta[0] - type(t0)(offset_start_x + k)
u_y = half_order + theta[1] - type(t0)(offset_start_y + k)
u_z = half_order + theta[2] - type(t0)(offset_start_z + k)
wx[k] = bspline_weight(u_x, ORDER)
wy[k] = bspline_weight(u_y, ORDER)
wz[k] = bspline_weight(u_z, ORDER)
dwx[k] = bspline_derivative(u_x, ORDER) * type(t0)(mesh_dims[0])
dwy[k] = bspline_derivative(u_y, ORDER) * type(t0)(mesh_dims[1])
dwz[k] = bspline_derivative(u_z, ORDER) * type(t0)(mesh_dims[2])
# Register accumulators (no atomics).
phi_acc = zero
gx_acc = zero
gy_acc = zero
gz_acc = zero
# Triple loop — fully unrolled at compile time.
for i in range(ORDER):
gx = wrap_grid_index(base_grid[0] + offset_start_x + i, mesh_dims[0])
wxi = wx[i]
dwxi = dwx[i]
for j in range(ORDER):
gy = wrap_grid_index(base_grid[1] + offset_start_y + j, mesh_dims[1])
wij = wxi * wy[j]
dwxij_x = dwxi * wy[j]
dwxij_y = wxi * dwy[j]
for k in range(ORDER):
gz = wrap_grid_index(
base_grid[2] + offset_start_z + k, mesh_dims[2]
)
val = mesh[gx, gy, gz]
wzk = wz[k]
phi_acc = phi_acc + val * (wij * wzk)
gx_acc = gx_acc + val * (dwxij_x * wzk)
gy_acc = gy_acc + val * (dwxij_y * wzk)
gz_acc = gz_acc + val * (wij * dwz[k])
# Single non-atomic write per output channel.
output[atom_idx] = phi_acc
grad_frac = _vec_pos(gx_acc, gy_acc, gz_acc)
force_frac = _vec_pos(
-charge * grad_frac[0],
-charge * grad_frac[1],
-charge * grad_frac[2],
)
forces[atom_idx] = wp.transpose(cell_inv_t[0]) * force_frac
return kernel
# Pre-compile per-order specializations for production orders.
# {scalar_dtype: {order: overload}}
_PER_ORDER_GATHER_WITH_FORCE_KERNELS: dict = {wp.float32: {}, wp.float64: {}}
_SUPPORTED_PER_ORDER = (2, 3, 4, 5, 6)
for _order in _SUPPORTED_PER_ORDER:
for _scalar, _vec, _mat in (
(wp.float32, wp.vec3f, wp.mat33f),
(wp.float64, wp.vec3d, wp.mat33d),
):
_k = _make_bspline_gather_with_force_kernel(_order, _scalar, _vec, _mat)
# Register a concrete-type overload so launch can resolve the kernel
# without inspecting the Any-typed annotations (which are strings under
# the file's `from __future__ import annotations`).
_PER_ORDER_GATHER_WITH_FORCE_KERNELS[_scalar][_order] = wp.overload(
_k,
[
wp.array(dtype=_vec), # positions
wp.array(dtype=_scalar), # charges
wp.array(dtype=_mat), # cell_inv_t
wp.array3d(dtype=_scalar), # mesh
wp.array(dtype=_scalar), # output
wp.array(dtype=_vec), # forces
],
)
def _make_batch_bspline_gather_with_force_kernel(
ORDER: int,
scalar_dtype,
vec_pos_dtype,
mat33_dtype,
):
"""Batched variant of ``_make_bspline_gather_with_force_kernel``.
Each thread handles one atom; the system index is looked up via
``batch_idx[atom_idx]`` and used to index the 4D mesh and per-system
inverse-cell. Same per-atom + register-accumulation pattern as the
single-system kernel; ORDER is a Python int literal so the inner
order^3 stencil loop unrolls fully at codegen time.
"""
vec_ord = _PER_ORDER_VEC[(ORDER, scalar_dtype)]
_vec_pos = vec_pos_dtype
HALF_ORDER_PY = float(ORDER) * 0.5
HALF_N_MINUS_2_PY = float(ORDER - 2) * 0.5
@wp.kernel(
module=_per_order_module(
"batch_gather_with_force",
ORDER,
scalar_dtype,
)
)
def kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz)
output: wp.array(dtype=Any),
forces: wp.array(dtype=Any),
):
atom_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
t0 = theta[0]
half_order = type(t0)(HALF_ORDER_PY)
half_n_minus_2 = type(t0)(HALF_N_MINUS_2_PY)
zero = type(t0)(0.0)
offset_start_x = wp.int32(wp.floor(theta[0] - half_n_minus_2))
offset_start_y = wp.int32(wp.floor(theta[1] - half_n_minus_2))
offset_start_z = wp.int32(wp.floor(theta[2] - half_n_minus_2))
wx = vec_ord()
wy = vec_ord()
wz = vec_ord() # noqa: E702
dwx = vec_ord()
dwy = vec_ord()
dwz = vec_ord() # noqa: E702
for k in range(ORDER):
u_x = half_order + theta[0] - type(t0)(offset_start_x + k)
u_y = half_order + theta[1] - type(t0)(offset_start_y + k)
u_z = half_order + theta[2] - type(t0)(offset_start_z + k)
wx[k] = bspline_weight(u_x, ORDER)
wy[k] = bspline_weight(u_y, ORDER)
wz[k] = bspline_weight(u_z, ORDER)
dwx[k] = bspline_derivative(u_x, ORDER) * type(t0)(mesh_dims[0])
dwy[k] = bspline_derivative(u_y, ORDER) * type(t0)(mesh_dims[1])
dwz[k] = bspline_derivative(u_z, ORDER) * type(t0)(mesh_dims[2])
phi_acc = zero
gx_acc = zero
gy_acc = zero
gz_acc = zero
for i in range(ORDER):
gx = wrap_grid_index(base_grid[0] + offset_start_x + i, mesh_dims[0])
wxi = wx[i]
dwxi = dwx[i]
for j in range(ORDER):
gy = wrap_grid_index(base_grid[1] + offset_start_y + j, mesh_dims[1])
wij = wxi * wy[j]
dwxij_x = dwxi * wy[j]
dwxij_y = wxi * dwy[j]
for k in range(ORDER):
gz = wrap_grid_index(
base_grid[2] + offset_start_z + k, mesh_dims[2]
)
val = mesh[sys_idx, gx, gy, gz]
wzk = wz[k]
phi_acc = phi_acc + val * (wij * wzk)
gx_acc = gx_acc + val * (dwxij_x * wzk)
gy_acc = gy_acc + val * (dwxij_y * wzk)
gz_acc = gz_acc + val * (wij * dwz[k])
output[atom_idx] = phi_acc
grad_frac = _vec_pos(gx_acc, gy_acc, gz_acc)
force_frac = _vec_pos(
-charge * grad_frac[0],
-charge * grad_frac[1],
-charge * grad_frac[2],
)
forces[atom_idx] = wp.transpose(cell_inv_t[sys_idx]) * force_frac
return kernel
# Pre-compile batch per-order specializations alongside the single-system ones.
_PER_ORDER_BATCH_GATHER_WITH_FORCE_KERNELS: dict = {wp.float32: {}, wp.float64: {}}
for _order in _SUPPORTED_PER_ORDER:
for _scalar, _vec, _mat in (
(wp.float32, wp.vec3f, wp.mat33f),
(wp.float64, wp.vec3d, wp.mat33d),
):
_k = _make_batch_bspline_gather_with_force_kernel(_order, _scalar, _vec, _mat)
_PER_ORDER_BATCH_GATHER_WITH_FORCE_KERNELS[_scalar][_order] = wp.overload(
_k,
[
wp.array(dtype=_vec), # positions
wp.array(dtype=_scalar), # charges
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=_mat), # cell_inv_t (B, 3, 3)
wp.array(dtype=_scalar, ndim=4), # mesh (B, nx, ny, nz)
wp.array(dtype=_scalar), # output
wp.array(dtype=_vec), # forces
],
)
###########################################################################################
########################### Per-order spread kernels #######################################
###########################################################################################
#
# One-thread-per-atom spread with fully unrolled order^3 stencil. Atomic
# adds into ``mesh`` are still required (atoms can share cells).
def _make_bspline_spread_kernel(
ORDER: int,
scalar_dtype,
vec_pos_dtype,
mat33_dtype,
):
"""Factory: per-order specialized single-system spread kernel.
One thread per atom, 1D weights in registers (``ORDER`` scalars per
axis), fully-unrolled order^3 inner loop. Eliminates the
per-(atom, stencil_pt) thread
explosion of the generic ``_bspline_spread_kernel`` (which spawns
``num_atoms * order^3`` threads each computing one atomic_add).
"""
vec_ord = _PER_ORDER_VEC[(ORDER, scalar_dtype)]
HALF_ORDER_PY = float(ORDER) * 0.5
HALF_N_MINUS_2_PY = float(ORDER - 2) * 0.5
@wp.kernel(module=_per_order_module("spread", ORDER, scalar_dtype))
def kernel(
positions: wp.array(dtype=Any),
values: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any), # (1, 3, 3)
mesh: wp.array3d(dtype=Any), # (nx, ny, nz) — atomic-add target
):
atom_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
value = values[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
t0 = theta[0]
half_order = type(t0)(HALF_ORDER_PY)
half_n_minus_2 = type(t0)(HALF_N_MINUS_2_PY)
offset_start_x = wp.int32(wp.floor(theta[0] - half_n_minus_2))
offset_start_y = wp.int32(wp.floor(theta[1] - half_n_minus_2))
offset_start_z = wp.int32(wp.floor(theta[2] - half_n_minus_2))
# 1D B-spline weights in registers (3 × ORDER scalars; no derivatives
# are needed for forward spread).
wx = vec_ord()
wy = vec_ord()
wz = vec_ord()
for k in range(ORDER):
u_x = half_order + theta[0] - type(t0)(offset_start_x + k)
u_y = half_order + theta[1] - type(t0)(offset_start_y + k)
u_z = half_order + theta[2] - type(t0)(offset_start_z + k)
wx[k] = bspline_weight(u_x, ORDER)
wy[k] = bspline_weight(u_y, ORDER)
wz[k] = bspline_weight(u_z, ORDER)
# Fully-unrolled stencil walk; ORDER is a Python int literal so
# Warp's codegen unrolls all three loops at compile time.
for i in range(ORDER):
gx = wrap_grid_index(base_grid[0] + offset_start_x + i, mesh_dims[0])
wxi = wx[i]
for j in range(ORDER):
gy = wrap_grid_index(base_grid[1] + offset_start_y + j, mesh_dims[1])
wij = wxi * wy[j]
for k in range(ORDER):
gz = wrap_grid_index(
base_grid[2] + offset_start_z + k, mesh_dims[2]
)
weight = wij * wz[k]
if weight > type(value)(1e-8):
wp.atomic_add(mesh, gx, gy, gz, value * weight)
return kernel
def _make_batch_bspline_spread_kernel(
ORDER: int,
scalar_dtype,
vec_pos_dtype,
mat33_dtype,
):
"""Batched variant of ``_make_bspline_spread_kernel``."""
vec_ord = _PER_ORDER_VEC[(ORDER, scalar_dtype)]
HALF_ORDER_PY = float(ORDER) * 0.5
HALF_N_MINUS_2_PY = float(ORDER - 2) * 0.5
@wp.kernel(module=_per_order_module("batch_spread", ORDER, scalar_dtype))
def kernel(
positions: wp.array(dtype=Any),
values: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz)
):
atom_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
value = values[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
t0 = theta[0]
half_order = type(t0)(HALF_ORDER_PY)
half_n_minus_2 = type(t0)(HALF_N_MINUS_2_PY)
offset_start_x = wp.int32(wp.floor(theta[0] - half_n_minus_2))
offset_start_y = wp.int32(wp.floor(theta[1] - half_n_minus_2))
offset_start_z = wp.int32(wp.floor(theta[2] - half_n_minus_2))
wx = vec_ord()
wy = vec_ord()
wz = vec_ord()
for k in range(ORDER):
u_x = half_order + theta[0] - type(t0)(offset_start_x + k)
u_y = half_order + theta[1] - type(t0)(offset_start_y + k)
u_z = half_order + theta[2] - type(t0)(offset_start_z + k)
wx[k] = bspline_weight(u_x, ORDER)
wy[k] = bspline_weight(u_y, ORDER)
wz[k] = bspline_weight(u_z, ORDER)
for i in range(ORDER):
gx = wrap_grid_index(base_grid[0] + offset_start_x + i, mesh_dims[0])
wxi = wx[i]
for j in range(ORDER):
gy = wrap_grid_index(base_grid[1] + offset_start_y + j, mesh_dims[1])
wij = wxi * wy[j]
for k in range(ORDER):
gz = wrap_grid_index(
base_grid[2] + offset_start_z + k, mesh_dims[2]
)
weight = wij * wz[k]
if weight > type(value)(1e-8):
wp.atomic_add(mesh, sys_idx, gx, gy, gz, value * weight)
return kernel
# Pre-compile per-order spread specializations for orders 2-6.
_PER_ORDER_SPREAD_KERNELS: dict = {wp.float32: {}, wp.float64: {}}
_PER_ORDER_BATCH_SPREAD_KERNELS: dict = {wp.float32: {}, wp.float64: {}}
for _order in _SUPPORTED_PER_ORDER:
for _scalar, _vec, _mat in (
(wp.float32, wp.vec3f, wp.mat33f),
(wp.float64, wp.vec3d, wp.mat33d),
):
_k = _make_bspline_spread_kernel(_order, _scalar, _vec, _mat)
_PER_ORDER_SPREAD_KERNELS[_scalar][_order] = wp.overload(
_k,
[
wp.array(dtype=_vec), # positions
wp.array(dtype=_scalar), # values
wp.array(dtype=_mat), # cell_inv_t (1, 3, 3)
wp.array3d(dtype=_scalar), # mesh
],
)
_kb = _make_batch_bspline_spread_kernel(_order, _scalar, _vec, _mat)
_PER_ORDER_BATCH_SPREAD_KERNELS[_scalar][_order] = wp.overload(
_kb,
[
wp.array(dtype=_vec), # positions
wp.array(dtype=_scalar), # values
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=_mat), # cell_inv_t (B, 3, 3)
wp.array(dtype=_scalar, ndim=4), # mesh (B, nx, ny, nz)
],
)
@wp.kernel
def _bspline_gather_gradient_kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array3d(dtype=Any),
forces: wp.array(dtype=Any),
):
r"""Compute forces by gathering mesh gradients using B-spline derivatives.
Computes:
.. math::
F_i = -q_i \sum_g \phi(g) \nabla w(r_i, g)
The gradient :math:`\nabla w` is computed in fractional coordinates and then
transformed to Cartesian coordinates via the cell matrix.
Launch Grid
-----------
dim = [num_atoms, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates in Cartesian space.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges.
cell_inv_t : wp.array, shape (1, 3, 3), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix for fractional coordinate conversion.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array3d, shape (nx, ny, nz), dtype=wp.float32 or wp.float64
3D mesh containing potential values (e.g., electrostatic potential phi).
forces : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Forces per atom in Cartesian coordinates. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's force.
- The gradient is computed in fractional coordinates, then transformed:
F_cart = cell_inv_t^T * F_frac
- Threads with zero gradient magnitude skip the atomic add for efficiency.
- Grid indices are wrapped using periodic boundary conditions.
"""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
grad_frac = bspline_weight_gradient_3d(theta, offset, order, mesh_dims)
grad_mag = wp.abs(grad_frac[0]) + wp.abs(grad_frac[1]) + wp.abs(grad_frac[2])
if grad_mag > type(charge)(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[gx, gy, gz]
force_frac = type(position)(
-charge * mesh_val * grad_frac[0],
-charge * mesh_val * grad_frac[1],
-charge * mesh_val * grad_frac[2],
)
force = wp.transpose(cell_inv_t[0]) * force_frac
wp.atomic_add(forces, atom_idx, force)
###########################################################################################
########################### spread-with-gradient-weights ###################################
###########################################################################################
#
# Backward of ``_bspline_gather_gradient_kernel`` w.r.t. ``mesh`` requires
# accumulating per-atom 3-vec scaling factors onto the mesh using the same
# B-spline gradient weights ∇W_frac that the forward gradient kernel used.
# This is the analog of ``_bspline_spread_kernel`` but with gradient
# weights instead of value weights.
#
# Given a per-atom 3-vec ``per_atom_vec[n,:]``:
# mesh[g] += Σ_d per_atom_vec[n, d] · ∇W_frac[d](x_n, g)
#
# Used in the backward of ``_bspline_gather_gradient_kernel`` with
# ``per_atom_vec[n] = -charges[n] · (cell_inv_t @ grad_force[n])``.
@wp.kernel
def _bspline_spread_gradient_weights_kernel(
positions: wp.array(dtype=Any), # (N,) vec3
per_atom_vec: wp.array(dtype=Any), # (N,) vec3
cell_inv_t: wp.array(dtype=Any), # (1,) mat33
order: wp.int32,
mesh: wp.array3d(dtype=Any), # (nx, ny, nz) output (zero-initialized)
):
"""Single-system "spread-with-gradient-weights" kernel.
For each ``(atom, support point)``:
mesh[g] += sum_d per_atom_vec[n, d] * grad_W_frac[d](x_n, g)
"""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
vec = per_atom_vec[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
grad_frac = bspline_weight_gradient_3d(theta, offset, order, mesh_dims)
grad_mag = wp.abs(grad_frac[0]) + wp.abs(grad_frac[1]) + wp.abs(grad_frac[2])
if grad_mag > type(vec[0])(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
contrib = vec[0] * grad_frac[0] + vec[1] * grad_frac[1] + vec[2] * grad_frac[2]
wp.atomic_add(mesh, gx, gy, gz, contrib)
@wp.kernel
def _batch_bspline_spread_gradient_weights_kernel(
positions: wp.array(dtype=Any), # (N_total,) vec3
per_atom_vec: wp.array(dtype=Any), # (N_total,) vec3
batch_idx: wp.array(dtype=wp.int32), # (N_total,)
cell_inv_t: wp.array(dtype=Any), # (B,) mat33
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz) output
):
"""Batched spread-with-gradient-weights kernel."""
atom_idx, point_idx = wp.tid()
system_id = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
vec = per_atom_vec[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[system_id], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
grad_frac = bspline_weight_gradient_3d(theta, offset, order, mesh_dims)
grad_mag = wp.abs(grad_frac[0]) + wp.abs(grad_frac[1]) + wp.abs(grad_frac[2])
if grad_mag > type(vec[0])(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
contrib = vec[0] * grad_frac[0] + vec[1] * grad_frac[1] + vec[2] * grad_frac[2]
wp.atomic_add(mesh, system_id, gx, gy, gz, contrib)
###########################################################################################
########################### gather_gradient position-Hessian kernel ########################
###########################################################################################
#
# Backward of ``_bspline_gather_gradient_kernel`` w.r.t. ``positions`` requires
# the spatial Hessian of the B-spline weight. Given an upstream cotangent
# ``grad_force`` (3-vec per atom) and the original mesh:
#
# ∂force[n,a]/∂position[n,b] = -q · Σ_g mesh[g] · (cell_inv_t.T H_scaled cell_inv_t)[a,b]
# grad_position[n,b] = -q · Σ_g mesh[g] · (cell_inv_t.T H_scaled v)[b]
#
# where ``H_scaled[c,d] = mesh_dims[c] · ∂²W/∂θ_c∂θ_d · mesh_dims[d]`` and
# ``v = cell_inv_t · grad_force[n]`` (precomputed per atom).
@wp.kernel
def _bspline_gather_gradient_position_hessian_kernel(
positions: wp.array(dtype=Any), # (N,) vec3
charges: wp.array(dtype=Any), # (N,)
v_per_atom: wp.array(dtype=Any), # (N,) vec3 — cell_inv_t @ grad_force
cell_inv_t: wp.array(dtype=Any), # (1,) mat33
order: wp.int32,
mesh: wp.array3d(dtype=Any), # original forward-input mesh
grad_positions: wp.array(dtype=Any), # (N,) vec3 output — zero-initialized
):
"""Single-system position-Hessian backward of ``_bspline_gather_gradient_kernel``."""
atom_idx, point_idx = wp.tid()
mesh_dims = wp.vec3i(mesh.shape[0], mesh.shape[1], mesh.shape[2])
position = positions[atom_idx]
q = charges[atom_idx]
v = v_per_atom[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
# H_scaled @ v (3-vec). Returns zero outside support.
Hv = bspline_weight_hessian_dot_vec3(theta, offset, order, mesh_dims, v)
mag = wp.abs(Hv[0]) + wp.abs(Hv[1]) + wp.abs(Hv[2])
if mag > type(q)(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[gx, gy, gz]
# contribution = -q · mesh · (cell_inv_t.T @ Hv)
cart = wp.transpose(cell_inv_t[0]) * Hv
scale = -q * mesh_val
wp.atomic_add(
grad_positions,
atom_idx,
type(position)(scale * cart[0], scale * cart[1], scale * cart[2]),
)
@wp.kernel
def _batch_bspline_gather_gradient_position_hessian_kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
v_per_atom: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4),
grad_positions: wp.array(dtype=Any),
):
"""Batched position-Hessian backward."""
atom_idx, point_idx = wp.tid()
system_id = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
q = charges[atom_idx]
v = v_per_atom[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[system_id], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
Hv = bspline_weight_hessian_dot_vec3(theta, offset, order, mesh_dims, v)
mag = wp.abs(Hv[0]) + wp.abs(Hv[1]) + wp.abs(Hv[2])
if mag > type(q)(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[system_id, gx, gy, gz]
cart = wp.transpose(cell_inv_t[system_id]) * Hv
scale = -q * mesh_val
wp.atomic_add(
grad_positions,
atom_idx,
type(position)(scale * cart[0], scale * cart[1], scale * cart[2]),
)
###########################################################################################
########################### Batch Warp Kernels #############################################
###########################################################################################
@wp.kernel
def _batch_bspline_spread_kernel(
positions: wp.array(dtype=Any),
values: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz)
):
"""Spread values from atoms to a batched 4D mesh using B-splines.
Batched version of _bspline_spread_kernel for multiple systems. Each atom
is assigned to a system via batch_idx, and values are spread to that
system's mesh slice.
Formula: mesh[sys, g] += value[atom] * w(atom, g)
Launch Grid
-----------
dim = [num_atoms_total, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates for all systems concatenated.
values : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Values to spread (e.g., charges) for all systems.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
cell_inv_t : wp.array, shape (B, 3, 3), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array4d, shape (B, nx, ny, nz), dtype=wp.float32 or wp.float64
OUTPUT: 4D mesh (batch x spatial) to accumulate values. Must be zero-initialized.
Notes
-----
- Uses atomic adds for thread-safe accumulation to shared grid points.
- Each system uses its own cell matrix for fractional coordinate conversion.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic add for efficiency.
"""
atom_idx, point_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
value = values[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
if weight > type(value)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
wp.atomic_add(mesh, sys_idx, gx, gy, gz, value * weight)
@wp.kernel
def _batch_bspline_gather_kernel(
positions: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz)
output: wp.array(dtype=Any),
):
"""Gather values from a batched 4D mesh to atom positions using B-splines.
Batched version of _bspline_gather_kernel for multiple systems. Each atom
reads from its assigned system's mesh slice via batch_idx.
Formula: output[atom] = sum_g mesh[sys, g] * w(atom, g)
Launch Grid
-----------
dim = [num_atoms_total, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates for all systems concatenated.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
cell_inv_t : wp.array, shape (B, 3, 3), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array4d, shape (B, nx, ny, nz), dtype=wp.float32 or wp.float64
4D mesh (batch x spatial) containing values to interpolate.
output : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Interpolated values per atom. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's output.
- Each system uses its own cell matrix for fractional coordinate conversion.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic add for efficiency.
"""
atom_idx, point_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
mesh_val = mesh[0, 0, 0, 0] # Get type reference
if weight > type(mesh_val)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[sys_idx, gx, gy, gz]
wp.atomic_add(output, atom_idx, mesh_val * weight)
@wp.kernel
def _batch_bspline_gather_vec3_kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz)
output: wp.array(dtype=Any),
):
"""Gather charge-weighted 3D vector values from batched mesh using B-splines.
Batched version of _bspline_gather_vec3_kernel for multiple systems.
Formula: output[atom] = q[atom] * sum_g mesh[sys, g] * w(atom, g)
Launch Grid
-----------
dim = [num_atoms_total, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates for all systems concatenated.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges (or other scalar weights).
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
cell_inv_t : wp.array, shape (B, 3, 3), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array4d, shape (B, nx, ny, nz), dtype=wp.vec3f or wp.vec3d
4D mesh (batch x spatial) containing vector values.
output : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Charge-weighted interpolated vectors per atom. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's output.
- Each system uses its own cell matrix for fractional coordinate conversion.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic add for efficiency.
"""
atom_idx, point_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
if weight > type(charge)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[sys_idx, gx, gy, gz]
wp.atomic_add(output, atom_idx, charge * mesh_val * weight)
@wp.kernel
def _batch_bspline_gather_gradient_kernel(
positions: wp.array(dtype=Any),
charges: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (B, nx, ny, nz)
forces: wp.array(dtype=Any),
):
r"""Compute forces by gathering mesh gradients from batched mesh using B-spline derivatives.
Computes:
.. math::
F_i = -q_i \sum_g \phi(g) \nabla w(r_i, g)
The gradient :math:`\nabla w` is computed in fractional coordinates and then
transformed to Cartesian coordinates via each system's cell matrix.
Launch Grid
-----------
dim = [num_atoms_total, order^3]
Each thread handles one (atom, grid_point) pair within the atom's stencil.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates for all systems concatenated.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
cell_inv_t : wp.array, shape (B, 3, 3), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array4d, shape (B, nx, ny, nz), dtype=wp.float32 or wp.float64
4D mesh (batch x spatial) containing potential values.
forces : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Forces per atom in Cartesian coordinates. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's force.
- The gradient is computed in fractional coordinates, then transformed:
F_cart = cell_inv_t[sys]^T * F_frac
- Each system uses its own cell matrix for the transformation.
- Threads with zero gradient magnitude skip the atomic add for efficiency.
- Grid indices are wrapped using periodic boundary conditions.
"""
atom_idx, point_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
charge = charges[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
grad_frac = bspline_weight_gradient_3d(theta, offset, order, mesh_dims)
grad_mag = wp.abs(grad_frac[0]) + wp.abs(grad_frac[1]) + wp.abs(grad_frac[2])
if grad_mag > type(charge)(0.0):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
mesh_val = mesh[sys_idx, gx, gy, gz]
force_frac = type(position)(
-charge * mesh_val * grad_frac[0],
-charge * mesh_val * grad_frac[1],
-charge * mesh_val * grad_frac[2],
)
force = wp.transpose(cell_inv_t[sys_idx]) * force_frac
wp.atomic_add(forces, atom_idx, force)
###########################################################################################
########################### Multi-Channel Warp Kernels ####################################
###########################################################################################
@wp.kernel
def _bspline_spread_channels_kernel(
positions: wp.array(dtype=Any),
values: wp.array2d(dtype=Any), # (N, C)
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (C, nx, ny, nz)
):
"""Spread multi-channel values from atoms to mesh using B-splines.
Similar to _bspline_spread_kernel but handles multiple channels per atom,
useful for multipole moments (e.g., monopole + dipole + quadrupole).
Formula: mesh[c, g] += values[atom, c] * w(atom, g)
for each channel c = 0, 1, ..., C-1.
Launch Grid
-----------
dim = [num_atoms, order^3]
Each thread handles one (atom, grid_point) pair and iterates over all channels.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates in Cartesian space.
values : wp.array2d, shape (N, C), dtype=wp.float32 or wp.float64
Multi-channel values to spread (e.g., multipole moments).
cell_inv_t : wp.array, shape (1, 3, 3), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix for fractional coordinate conversion.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array4d, shape (C, nx, ny, nz), dtype=wp.float32 or wp.float64
OUTPUT: 4D mesh (channels x spatial) to accumulate values. Must be zero-initialized.
Notes
-----
- Uses atomic adds for thread-safe accumulation to shared grid points.
- Each channel is spread independently to its own mesh slice.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic adds for efficiency.
"""
atom_idx, point_idx = wp.tid()
num_channels = values.shape[1]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
val = values[0, 0] # Get type reference
if weight > type(val)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
# Spread each channel
for c in range(num_channels):
val = values[atom_idx, c]
wp.atomic_add(mesh, c, gx, gy, gz, val * weight)
@wp.kernel
def _bspline_gather_channels_kernel(
positions: wp.array(dtype=Any),
cell_inv_t: wp.array(dtype=Any),
order: wp.int32,
mesh: wp.array(dtype=Any, ndim=4), # (C, nx, ny, nz)
output: wp.array2d(dtype=Any), # (N, C)
):
"""Gather multi-channel values from mesh to atoms using B-splines.
Similar to _bspline_gather_kernel but handles multiple channels,
useful for multipole-based methods.
Formula: output[atom, c] = sum_g mesh[c, g] * w(atom, g)
for each channel c = 0, 1, ..., C-1.
Launch Grid
-----------
dim = [num_atoms, order^3]
Each thread handles one (atom, grid_point) pair and iterates over all channels.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates in Cartesian space.
cell_inv_t : wp.array, shape (1, 3, 3), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix for fractional coordinate conversion.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
mesh : wp.array4d, shape (C, nx, ny, nz), dtype=wp.float32 or wp.float64
4D mesh (channels x spatial) containing values to interpolate.
output : wp.array2d, shape (N, C), dtype=wp.float32 or wp.float64
OUTPUT: Interpolated multi-channel values per atom. Must be zero-initialized.
Notes
-----
- Uses atomic adds since multiple threads contribute to each atom's output.
- Each channel is gathered independently from its own mesh slice.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic adds for efficiency.
"""
atom_idx, point_idx = wp.tid()
num_channels = mesh.shape[0]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
base_grid, theta = compute_fractional_coords(position, cell_inv_t[0], mesh_dims)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
mesh_val = mesh[0, 0, 0, 0] # Get type reference
if weight > type(mesh_val)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
# Gather each channel
for c in range(num_channels):
mesh_val = mesh[c, gx, gy, gz]
wp.atomic_add(output, atom_idx, c, mesh_val * weight)
@wp.kernel
def _batch_bspline_spread_channels_kernel(
positions: wp.array(dtype=Any),
values: wp.array2d(dtype=Any), # (N, C)
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
order: wp.int32,
num_channels: wp.int32,
mesh: wp.array4d(dtype=Any), # (B*C, nx, ny, nz) - flattened batch*channel
):
"""Spread multi-channel values from atoms to batched mesh using B-splines.
Batched version of _bspline_spread_channels_kernel. Due to Warp's 4D array
limit, the batch and channel dimensions are flattened into a single dimension.
Formula: mesh[sys*C + c, g] += values[atom, c] * w(atom, g)
Launch Grid
-----------
dim = [num_atoms_total, order^3]
Each thread handles one (atom, grid_point) pair and iterates over all channels.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates for all systems concatenated.
values : wp.array2d, shape (N_total, C), dtype=wp.float32 or wp.float64
Multi-channel values to spread (e.g., multipole moments).
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
cell_inv_t : wp.array, shape (B, 3, 3), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
num_channels : wp.int32
Number of channels (C).
mesh : wp.array4d, shape (B*C, nx, ny, nz), dtype=wp.float32 or wp.float64
OUTPUT: Flattened 4D mesh to accumulate values. Must be zero-initialized.
Notes
-----
- Mesh storage: (B*C, nx, ny, nz) with flat_idx = sys_idx * C + channel_idx.
- Uses atomic adds for thread-safe accumulation to shared grid points.
- Each system uses its own cell matrix for fractional coordinate conversion.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic adds for efficiency.
"""
atom_idx, point_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
val = values[0, 0] # Get type reference
if weight > type(val)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
# Spread each channel using flattened batch*channel indexing
for c in range(num_channels):
flat_idx = sys_idx * num_channels + c
val = values[atom_idx, c]
wp.atomic_add(mesh, flat_idx, gx, gy, gz, val * weight)
@wp.kernel
def _batch_bspline_gather_channels_kernel(
positions: wp.array(dtype=Any),
batch_idx: wp.array(dtype=wp.int32),
cell_inv_t: wp.array(dtype=Any), # (B, 3, 3)
order: wp.int32,
num_channels: wp.int32,
mesh: wp.array4d(dtype=Any), # (B*C, nx, ny, nz) - flattened batch*channel
output: wp.array2d(dtype=Any), # (N, C)
):
"""Gather multi-channel values from batched mesh to atoms using B-splines.
Batched version of _bspline_gather_channels_kernel. Due to Warp's 4D array
limit, the batch and channel dimensions are flattened into a single dimension.
Formula: output[atom, c] = sum_g mesh[sys*C + c, g] * w(atom, g)
Launch Grid
-----------
dim = [num_atoms_total, order^3]
Each thread handles one (atom, grid_point) pair and iterates over all channels.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic coordinates for all systems concatenated.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom (0 to B-1).
cell_inv_t : wp.array, shape (B, 3, 3), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : wp.int32
B-spline order (1-6). Order 4 (cubic) recommended for PME.
num_channels : wp.int32
Number of channels (C).
mesh : wp.array4d, shape (B*C, nx, ny, nz), dtype=wp.float32 or wp.float64
Flattened 4D mesh (batch*channels x spatial) containing values.
output : wp.array2d, shape (N_total, C), dtype=wp.float32 or wp.float64
OUTPUT: Interpolated multi-channel values per atom. Must be zero-initialized.
Notes
-----
- Mesh storage: (B*C, nx, ny, nz) with flat_idx = sys_idx * C + channel_idx.
- Uses atomic adds since multiple threads contribute to each atom's output.
- Each system uses its own cell matrix for fractional coordinate conversion.
- Grid indices are wrapped using periodic boundary conditions.
- Threads with 1e-8 weight or less skip the atomic adds for efficiency.
"""
atom_idx, point_idx = wp.tid()
sys_idx = batch_idx[atom_idx]
mesh_dims = wp.vec3i(mesh.shape[1], mesh.shape[2], mesh.shape[3])
position = positions[atom_idx]
base_grid, theta = compute_fractional_coords(
position, cell_inv_t[sys_idx], mesh_dims
)
offset = bspline_grid_offset(point_idx, order, theta)
weight = bspline_weight_3d(theta, offset, order)
mesh_val = mesh[0, 0, 0, 0] # Get type reference
if weight > type(mesh_val)(1e-8):
gx = wrap_grid_index(base_grid[0] + offset[0], mesh_dims[0])
gy = wrap_grid_index(base_grid[1] + offset[1], mesh_dims[1])
gz = wrap_grid_index(base_grid[2] + offset[2], mesh_dims[2])
# Gather each channel using flattened batch*channel indexing
for c in range(num_channels):
flat_idx = sys_idx * num_channels + c
mesh_val = mesh[flat_idx, gx, gy, gz]
wp.atomic_add(output, atom_idx, c, mesh_val * weight)
###########################################################################################
########################### Kernel Overloads for Dtype Flexibility #########################
###########################################################################################
# Type lists for creating overloads
_T = [wp.float32, wp.float64]
_V = [wp.vec3f, wp.vec3d]
_M = [wp.mat33f, wp.mat33d]
# Single-system kernel overloads
_bspline_weight_kernel_overload = {}
_bspline_spread_kernel_overload = {}
_bspline_gather_kernel_overload = {}
_bspline_gather_vec3_kernel_overload = {}
_bspline_gather_gradient_kernel_overload = {}
_bspline_gather_with_force_kernel_overload = {}
_bspline_spread_gradient_weights_kernel_overload = {}
_bspline_gather_gradient_position_hessian_kernel_overload = {}
# Batch kernel overloads
_batch_bspline_spread_kernel_overload = {}
_batch_bspline_gather_kernel_overload = {}
_batch_bspline_gather_vec3_kernel_overload = {}
_batch_bspline_gather_gradient_kernel_overload = {}
_batch_bspline_spread_gradient_weights_kernel_overload = {}
_batch_bspline_gather_gradient_position_hessian_kernel_overload = {}
# Multi-channel kernel overloads
_bspline_spread_channels_kernel_overload = {}
_bspline_gather_channels_kernel_overload = {}
_batch_bspline_spread_channels_kernel_overload = {}
_batch_bspline_gather_channels_kernel_overload = {}
for t, v, m in zip(_T, _V, _M):
# Single-system kernels
_bspline_weight_kernel_overload[t] = wp.overload(
_bspline_weight_kernel,
[
wp.array(dtype=t), # u
wp.int32, # order
wp.array(dtype=t), # weights
],
)
_bspline_spread_kernel_overload[t] = wp.overload(
_bspline_spread_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # values
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array3d(dtype=t), # mesh
],
)
_bspline_gather_kernel_overload[t] = wp.overload(
_bspline_gather_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array3d(dtype=t), # mesh
wp.array(dtype=t), # output
],
)
_bspline_gather_vec3_kernel_overload[t] = wp.overload(
_bspline_gather_vec3_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array3d(dtype=v), # mesh
wp.array(dtype=v), # output
],
)
_bspline_gather_gradient_kernel_overload[t] = wp.overload(
_bspline_gather_gradient_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array3d(dtype=t), # mesh
wp.array(dtype=v), # forces
],
)
_bspline_gather_with_force_kernel_overload[t] = wp.overload(
_bspline_gather_with_force_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array3d(dtype=t), # mesh
wp.array(dtype=t), # output (potential)
wp.array(dtype=v), # forces
],
)
_bspline_spread_gradient_weights_kernel_overload[t] = wp.overload(
_bspline_spread_gradient_weights_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=v), # per_atom_vec
wp.array(dtype=m), # cell_inv_t (1,)
wp.int32, # order
wp.array3d(dtype=t), # mesh
],
)
_bspline_gather_gradient_position_hessian_kernel_overload[t] = wp.overload(
_bspline_gather_gradient_position_hessian_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=v), # v_per_atom
wp.array(dtype=m), # cell_inv_t (1,)
wp.int32, # order
wp.array3d(dtype=t), # mesh
wp.array(dtype=v), # grad_positions
],
)
# Batch kernels
_batch_bspline_spread_kernel_overload[t] = wp.overload(
_batch_bspline_spread_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # values
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh
],
)
_batch_bspline_gather_kernel_overload[t] = wp.overload(
_batch_bspline_gather_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh
wp.array(dtype=t), # output
],
)
_batch_bspline_gather_vec3_kernel_overload[t] = wp.overload(
_batch_bspline_gather_vec3_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array(dtype=v, ndim=4), # mesh
wp.array(dtype=v), # output
],
)
_batch_bspline_gather_gradient_kernel_overload[t] = wp.overload(
_batch_bspline_gather_gradient_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh
wp.array(dtype=v), # forces
],
)
_batch_bspline_spread_gradient_weights_kernel_overload[t] = wp.overload(
_batch_bspline_spread_gradient_weights_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=v), # per_atom_vec
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t (B,)
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh (B, nx, ny, nz)
],
)
_batch_bspline_gather_gradient_position_hessian_kernel_overload[t] = wp.overload(
_batch_bspline_gather_gradient_position_hessian_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=t), # charges
wp.array(dtype=v), # v_per_atom
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t (B,)
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh
wp.array(dtype=v), # grad_positions
],
)
# Multi-channel kernels
_bspline_spread_channels_kernel_overload[t] = wp.overload(
_bspline_spread_channels_kernel,
[
wp.array(dtype=v), # positions
wp.array2d(dtype=t), # values
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh
],
)
_bspline_gather_channels_kernel_overload[t] = wp.overload(
_bspline_gather_channels_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.array(dtype=t, ndim=4), # mesh
wp.array2d(dtype=t), # output
],
)
_batch_bspline_spread_channels_kernel_overload[t] = wp.overload(
_batch_bspline_spread_channels_kernel,
[
wp.array(dtype=v), # positions
wp.array2d(dtype=t), # values
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.int32, # num_channels
wp.array4d(dtype=t), # mesh
],
)
_batch_bspline_gather_channels_kernel_overload[t] = wp.overload(
_batch_bspline_gather_channels_kernel,
[
wp.array(dtype=v), # positions
wp.array(dtype=wp.int32), # batch_idx
wp.array(dtype=m), # cell_inv_t
wp.int32, # order
wp.int32, # num_channels
wp.array4d(dtype=t), # mesh
wp.array2d(dtype=t), # output
],
)
###########################################################################################
########################### Warp Launcher Functions #######################################
###########################################################################################
def bspline_weight_launcher(
u: wp.array,
order: int,
weights: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Compute B-spline weights for an array of inputs.
Parameters
----------
u : wp.array, shape (N,)
Input values.
order : int
B-spline order.
weights : wp.array, shape (N,)
Output weights.
wp_dtype : type
Warp scalar dtype.
device : str | None
Warp device string.
"""
num_points = u.shape[0]
kernel = _bspline_weight_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=num_points,
inputs=[u, wp.int32(order)],
outputs=[weights],
device=device,
)
[docs]
def spline_spread(
positions: wp.array,
values: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Spread values from atoms to mesh using B-spline interpolation.
Framework-agnostic launcher for single-system spline spread.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic positions.
values : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Values to spread (e.g., charges).
cell_inv_t : wp.array, shape (1,), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (nx, ny, nz), dtype=wp.float32 or wp.float64
OUTPUT: Mesh to accumulate values. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _bspline_spread_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, values, cell_inv_t, wp.int32(order)],
outputs=[mesh],
device=device,
)
[docs]
def spline_gather(
positions: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
output: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Gather values from mesh to atoms using B-spline interpolation.
Framework-agnostic launcher for single-system spline gather.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic positions.
cell_inv_t : wp.array, shape (1,), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (nx, ny, nz), dtype=wp.float32 or wp.float64
Mesh to interpolate from.
output : wp.array, shape (N,), dtype=wp.float32 or wp.float64
OUTPUT: Interpolated values per atom. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _bspline_gather_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, cell_inv_t, wp.int32(order), mesh],
outputs=[output],
device=device,
)
[docs]
def spline_gather_vec3(
positions: wp.array,
charges: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
output: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Gather charge-weighted vector values from mesh using B-splines.
Framework-agnostic launcher for single-system vec3 spline gather.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic positions.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges.
cell_inv_t : wp.array, shape (1,), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (nx, ny, nz), dtype=wp.vec3f or wp.vec3d
Vector-valued mesh to interpolate from.
output : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Charge-weighted interpolated vectors. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _bspline_gather_vec3_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, charges, cell_inv_t, wp.int32(order), mesh],
outputs=[output],
device=device,
)
[docs]
def spline_gather_gradient(
positions: wp.array,
charges: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
forces: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Compute forces using B-spline gradient interpolation.
Framework-agnostic launcher for single-system spline gradient gather.
Parameters
----------
positions : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
Atomic positions.
charges : wp.array, shape (N,), dtype=wp.float32 or wp.float64
Atomic charges.
cell_inv_t : wp.array, shape (1,), dtype=wp.mat33f or wp.mat33d
Transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (nx, ny, nz), dtype=wp.float32 or wp.float64
Potential mesh.
forces : wp.array, shape (N,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Forces per atom. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _bspline_gather_gradient_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, charges, cell_inv_t, wp.int32(order), mesh],
outputs=[forces],
device=device,
)
def spline_spread_gradient_weights(
positions: wp.array,
per_atom_vec: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Single-system launcher for ``_bspline_spread_gradient_weights_kernel``.
``mesh`` output must be zero-initialized.
"""
kernel = _bspline_spread_gradient_weights_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(positions.shape[0], order**3),
inputs=[positions, per_atom_vec, cell_inv_t, wp.int32(order)],
outputs=[mesh],
device=device,
)
def spline_gather_gradient_position_hessian(
positions: wp.array,
charges: wp.array,
v_per_atom: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
grad_positions: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Single-system launcher for the position-Hessian backward of
``_bspline_gather_gradient_kernel``. ``grad_positions`` must be
zero-initialized.
"""
kernel = _bspline_gather_gradient_position_hessian_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(positions.shape[0], order**3),
inputs=[positions, charges, v_per_atom, cell_inv_t, wp.int32(order), mesh],
outputs=[grad_positions],
device=device,
)
def spline_gather_with_force(
positions: wp.array,
charges: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
output: wp.array,
forces: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Fused energy-gather + force-gather in a single kernel launch.
Computes simultaneously, reading each mesh cell ONCE:
- ``output[atom] = sum_g mesh[g] * w(atom, g)`` (raw potential)
- ``forces[atom] = -q_atom * sum_g mesh[g] * Cell^{-T} grad_w`` (Cartesian force)
Replaces the (``spline_gather`` -> ``spline_gather_gradient``) pair when
both outputs are needed (PME forces path). Halves the mesh DRAM traffic
and reuses the per-thread 1D weight evaluations across both outputs.
Output buffers must be zero-initialized.
"""
num_atoms = positions.shape[0]
# Per-order specialized kernel is available for orders 2-6 and uses
# register accumulation + compile-time unrolling instead of the generic
# runtime-order stencil loop.
per_order = _PER_ORDER_GATHER_WITH_FORCE_KERNELS[wp_dtype].get(order)
if per_order is not None:
wp.launch(
per_order,
dim=num_atoms,
inputs=[positions, charges, cell_inv_t, mesh],
outputs=[output, forces],
device=device,
)
else:
# Fallback: generic per-(atom, stencil-point) kernel with atomics.
kernel = _bspline_gather_with_force_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, order**3),
inputs=[positions, charges, cell_inv_t, wp.int32(order), mesh],
outputs=[output, forces],
device=device,
)
[docs]
def batch_spline_spread(
positions: wp.array,
values: wp.array,
batch_idx: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Spread values from atoms to batched mesh using B-spline interpolation.
Framework-agnostic launcher for batched spline spread.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic positions for all systems.
values : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Values to spread.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom.
cell_inv_t : wp.array, shape (B,), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (B, nx, ny, nz), dtype=wp.float32 or wp.float64
OUTPUT: Batched mesh to accumulate values. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _batch_bspline_spread_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, values, batch_idx, cell_inv_t, wp.int32(order)],
outputs=[mesh],
device=device,
)
[docs]
def batch_spline_gather(
positions: wp.array,
batch_idx: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
output: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Gather values from batched mesh to atoms using B-spline interpolation.
Framework-agnostic launcher for batched spline gather.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic positions for all systems.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom.
cell_inv_t : wp.array, shape (B,), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (B, nx, ny, nz), dtype=wp.float32 or wp.float64
Batched mesh to interpolate from.
output : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
OUTPUT: Interpolated values per atom. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _batch_bspline_gather_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, batch_idx, cell_inv_t, wp.int32(order), mesh],
outputs=[output],
device=device,
)
[docs]
def batch_spline_gather_vec3(
positions: wp.array,
charges: wp.array,
batch_idx: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
output: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Gather charge-weighted vector values from batched mesh using B-splines.
Framework-agnostic launcher for batched vec3 spline gather.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic positions for all systems.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom.
cell_inv_t : wp.array, shape (B,), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (B, nx, ny, nz), dtype=wp.vec3f or wp.vec3d
Batched vector mesh to interpolate from.
output : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Charge-weighted interpolated vectors. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _batch_bspline_gather_vec3_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, charges, batch_idx, cell_inv_t, wp.int32(order), mesh],
outputs=[output],
device=device,
)
[docs]
def batch_spline_gather_gradient(
positions: wp.array,
charges: wp.array,
batch_idx: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
forces: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Compute forces using B-spline gradient interpolation from batched mesh.
Framework-agnostic launcher for batched spline gradient gather.
Parameters
----------
positions : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
Atomic positions for all systems.
charges : wp.array, shape (N_total,), dtype=wp.float32 or wp.float64
Atomic charges.
batch_idx : wp.array, shape (N_total,), dtype=wp.int32
System index for each atom.
cell_inv_t : wp.array, shape (B,), dtype=wp.mat33f or wp.mat33d
Per-system transpose of inverse cell matrix.
order : int
B-spline order (1-6).
mesh : wp.array, shape (B, nx, ny, nz), dtype=wp.float32 or wp.float64
Batched potential mesh.
forces : wp.array, shape (N_total,), dtype=wp.vec3f or wp.vec3d
OUTPUT: Forces per atom. Must be zero-initialized.
wp_dtype : type
Warp scalar dtype (wp.float32 or wp.float64).
device : str | None
Warp device string. If None, inferred from arrays.
"""
num_atoms = positions.shape[0]
num_points = order**3
kernel = _batch_bspline_gather_gradient_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(num_atoms, num_points),
inputs=[positions, charges, batch_idx, cell_inv_t, wp.int32(order), mesh],
outputs=[forces],
device=device,
)
def batch_spline_spread_gradient_weights(
positions: wp.array,
per_atom_vec: wp.array,
batch_idx: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched launcher for ``_batch_bspline_spread_gradient_weights_kernel``.
``mesh`` output must be zero-initialized.
"""
kernel = _batch_bspline_spread_gradient_weights_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(positions.shape[0], order**3),
inputs=[positions, per_atom_vec, batch_idx, cell_inv_t, wp.int32(order)],
outputs=[mesh],
device=device,
)
def batch_spline_gather_gradient_position_hessian(
positions: wp.array,
charges: wp.array,
v_per_atom: wp.array,
batch_idx: wp.array,
cell_inv_t: wp.array,
order: int,
mesh: wp.array,
grad_positions: wp.array,
wp_dtype: type,
device: str | None = None,
) -> None:
"""Batched launcher for the position-Hessian backward of
``_bspline_gather_gradient_kernel``. ``grad_positions`` zero-initialized."""
kernel = _batch_bspline_gather_gradient_position_hessian_kernel_overload[wp_dtype]
wp.launch(
kernel,
dim=(positions.shape[0], order**3),
inputs=[
positions,
charges,
v_per_atom,
batch_idx,
cell_inv_t,
wp.int32(order),
mesh,
],
outputs=[grad_positions],
device=device,
)
###########################################################################################
########################### Module Exports #################################################
###########################################################################################
__all__ = [
# Warp functions (@wp.func)
"bspline_weight",
"bspline_derivative",
"bspline_second_derivative",
"bspline_third_derivative",
"bspline_fourth_derivative",
"bspline_weight_3d",
"bspline_weight_gradient_3d",
"bspline_weight_hessian_3d",
"bspline_weight_hessian_dot_vec3",
"compute_fractional_coords",
"bspline_grid_offset",
"wrap_grid_index",
# Warp launchers
"bspline_weight_launcher",
"spline_spread",
"spline_gather",
"spline_gather_vec3",
"spline_gather_gradient",
"spline_spread_gradient_weights",
"spline_gather_gradient_position_hessian",
"batch_spline_spread",
"batch_spline_gather",
"batch_spline_gather_vec3",
"batch_spline_gather_gradient",
"batch_spline_spread_gradient_weights",
"batch_spline_gather_gradient_position_hessian",
]