Source code for nvalchemiops.dynamics.integrators.npt

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

r"""
NPT and NPH Integrators with Isotropic and Anisotropic Pressure Control.

This module implements the Martyna-Tobias-Klein (MTK) equations of motion
for NPT and NPH molecular dynamics simulations.

NPT (Isothermal-Isobaric)
-------------------------
Constant temperature and pressure via coupled Nose-Hoover thermostat
and barostat. Samples the canonical NPT ensemble.

NPH (Isenthalpic-Isobaric)
--------------------------
Constant enthalpy and pressure without thermostat. The system evolves
on a constant enthalpy surface at fixed pressure.

Pressure Control Modes
----------------------
- Isotropic: Single pressure value, cell scales uniformly
- Anisotropic (orthorhombic): Independent x, y, z pressure control
- Anisotropic (triclinic): Full stress tensor control (9 components)

Conventions
-----------
**Cell velocities** (``cell_velocities``) are the strain-rate tensor
:math:`\dot{\varepsilon} = p_g / W` (canonical MTK / ASE / TorchSim convention).  The absolute
time derivative is :math:`\dot{h} = \dot{\varepsilon}\,\mathbf{h}`, applied multiplicatively in the cell
update step.  Callers should not pre-multiply by h.

**Virial sign**: ``compute_pressure_tensor`` expects the virial
:math:`W = -dE/d\varepsilon` as defined in ``docs/userguide/about/conventions.md``.
All interaction kernels in this library produce this sign directly.

References
----------
- Martyna, Tobias, Klein, J. Chem. Phys. 101, 4177 (1994)
- Shinoda, Shiga, Mikami, Phys. Rev. B 69, 134103 (2004)
- LAMMPS fix_nh documentation

All kernels are dtype-agnostic and support both float32 and float64.
"""

from __future__ import annotations

import os
import warnings
from typing import Any

import warp as wp

from nvalchemiops.dynamics.utils.cell_utils import (
    compute_cell_inverse,
    compute_cell_volume,
)
from nvalchemiops.dynamics.utils.launch_helpers import (
    KernelFamily,
    launch_family,
    resolve_execution_mode,
)
from nvalchemiops.dynamics.utils.thermostat_utils import compute_kinetic_energy
from nvalchemiops.warp_dispatch import validate_out_array

__all__ = [
    # Tensor types for pressure/virial
    "vec9f",
    "vec9d",
    "vec3f",
    "vec3d",
    # Pressure calculations
    "compute_pressure_tensor",
    "compute_scalar_pressure",
    # Barostat utilities
    "compute_barostat_mass",
    "compute_cell_kinetic_energy",
    "compute_barostat_potential_energy",
    # NPT integration steps - Mutating
    "npt_thermostat_half_step",
    "npt_barostat_half_step",  # Unified: dispatches based on target_pressures dtype
    "npt_velocity_half_step",  # Unified: dispatches based on mode parameter
    "npt_position_update",
    "npt_cell_update",
    # NPT integration steps - Non-mutating
    "npt_velocity_half_step_out",
    "npt_position_update_out",
    "npt_cell_update_out",
    # High-level NPT integration
    "run_npt_step",
    # NPH integration steps - Mutating
    "nph_barostat_half_step",  # Unified: dispatches based on target_pressures dtype
    "nph_velocity_half_step",  # Unified: dispatches based on mode parameter
    "nph_position_update",
    "nph_cell_update",
    # NPH integration steps - Non-mutating
    "nph_velocity_half_step_out",
    "nph_position_update_out",
    # High-level NPH integration
    "run_nph_step",
]


# ==============================================================================
# Tensor Types
# ==============================================================================

# 9-element vector types for symmetric tensor storage
# Order: [xx, xy, xz, yx, yy, yz, zx, zy, zz]
vec9f = wp.types.vector(length=9, dtype=wp.float32)
vec9d = wp.types.vector(length=9, dtype=wp.float64)

# 3-element vector types for anisotropic (orthorhombic) pressure
# Order: [Pxx, Pyy, Pzz]
vec3f = wp.vec3f
vec3d = wp.vec3d


# =============================================================================
# Helper: cells_inv compatibility shim
# =============================================================================


def _ensure_cells_inv(
    cells_inv: wp.array | None,
    volumes: wp.array | None,
    cell_velocities: wp.array,
    device: str | None,
) -> wp.array:
    """Return ``cells_inv`` if provided, else a zero placeholder for
    callers that still pass it; kernels no longer consume it."""
    if cells_inv is not None:
        return cells_inv
    if device is None:
        device = cell_velocities.device
    return wp.zeros(
        cell_velocities.shape[0], dtype=cell_velocities.dtype, device=device
    )


def _warn_cells_inv_deprecated(stacklevel: int = 2) -> None:
    """Emit a DeprecationWarning when callers still pass ``cells_inv``."""
    warnings.warn(
        "'cells_inv' is deprecated and ignored; kernels consume "
        "'cell_velocities' as the strain rate ε̇ = p_g/W. "
        "Will be removed in a future release.",
        DeprecationWarning,
        stacklevel=stacklevel,
    )


def _warn_volumes_deprecated(stacklevel: int = 2) -> None:
    """Emit a DeprecationWarning when callers still pass ``volumes``."""
    warnings.warn(
        "'volumes' is deprecated and ignored on this entry point; "
        "kernels consume 'cell_velocities' as the strain rate ε̇ = p_g/W "
        "and no longer need a volume fallback. "
        "Will be removed in a future release.",
        DeprecationWarning,
        stacklevel=stacklevel,
    )


# =============================================================================
# Shared @wp.func for NPT/NPH physics
# =============================================================================


@wp.func
def _npt_accel(f: Any, m: Any) -> Any:
    """Acceleration: F/m as vec3."""
    inv_mass = wp.where(m > type(m)(0.0), type(m)(1.0) / m, type(m)(0.0))
    return type(f)(f[0] * inv_mass, f[1] * inv_mass, f[2] * inv_mass)


@wp.func
def _drag_isotropic(eps_dot: Any, coupling: Any, eta_dot_1: Any, v: Any) -> Any:
    """Isotropic drag: ``(coupling * Tr(eps_dot)/3 + eta_dot_1) * v``."""
    trace_eps = eps_dot[0, 0] + eps_dot[1, 1] + eps_dot[2, 2]
    eps_scalar = trace_eps / type(trace_eps)(3.0)
    return (coupling * eps_scalar + eta_dot_1) * v


@wp.func
def _drag_anisotropic(eps_dot: Any, inv_three_n: Any, eta_dot_1: Any, v: Any) -> Any:
    """Anisotropic drag: ``(eps_dot_ii + Tr(eps_dot) * inv_three_n + eta_dot_1) * v_i``."""
    trace_eps = eps_dot[0, 0] + eps_dot[1, 1] + eps_dot[2, 2]
    trace_corr = trace_eps * inv_three_n
    return type(v)(
        (eps_dot[0, 0] + trace_corr + eta_dot_1) * v[0],
        (eps_dot[1, 1] + trace_corr + eta_dot_1) * v[1],
        (eps_dot[2, 2] + trace_corr + eta_dot_1) * v[2],
    )


@wp.func
def _drag_triclinic(eps_dot: Any, inv_three_n: Any, eta_dot_1: Any, v: Any) -> Any:
    """Triclinic drag: ``(eps_dot + (Tr(eps_dot) * inv_three_n + eta_dot_1) * I) @ v``."""
    trace_eps = eps_dot[0, 0] + eps_dot[1, 1] + eps_dot[2, 2]
    diag_corr = trace_eps * inv_three_n + eta_dot_1
    drag_x = (
        eps_dot[0, 0] * v[0]
        + eps_dot[0, 1] * v[1]
        + eps_dot[0, 2] * v[2]
        + diag_corr * v[0]
    )
    drag_y = (
        eps_dot[1, 0] * v[0]
        + eps_dot[1, 1] * v[1]
        + eps_dot[1, 2] * v[2]
        + diag_corr * v[1]
    )
    drag_z = (
        eps_dot[2, 0] * v[0]
        + eps_dot[2, 1] * v[1]
        + eps_dot[2, 2] * v[2]
        + diag_corr * v[2]
    )
    return type(v)(drag_x, drag_y, drag_z)


@wp.func
def _npt_position_step(r: Any, v: Any, eps_dot: Any, dt: Any) -> Any:
    """Position update: ``r + dt * (v + eps_dot @ r)``."""
    eps_dot_r = wp.mul(eps_dot, r)
    return r + dt * (v + eps_dot_r)


# ==============================================================================
# Pressure Calculation Kernels
# ==============================================================================

# Tile block size for cooperative reductions
TILE_DIM = int(os.getenv("NVALCHEMIOPS_DYNAMICS_TILE_DIM", 256))


@wp.kernel
def _compute_kinetic_tensor_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    kinetic_tensors: wp.array2d(dtype=Any),
):
    """Compute kinetic contribution to pressure tensor (single system).

    P_kin[i,j] = sum_k m_k * v_k[i] * v_k[j]

    Launch Grid: dim = [num_atoms]
    Note: Uses array2d for atomic operations, converted to vec9 after.
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]

    # Outer product: m * v ⊗ v
    wp.atomic_add(kinetic_tensors, 0, 0, m * v[0] * v[0])  # xx
    wp.atomic_add(kinetic_tensors, 0, 1, m * v[0] * v[1])  # xy
    wp.atomic_add(kinetic_tensors, 0, 2, m * v[0] * v[2])  # xz
    wp.atomic_add(kinetic_tensors, 0, 3, m * v[1] * v[0])  # yx
    wp.atomic_add(kinetic_tensors, 0, 4, m * v[1] * v[1])  # yy
    wp.atomic_add(kinetic_tensors, 0, 5, m * v[1] * v[2])  # yz
    wp.atomic_add(kinetic_tensors, 0, 6, m * v[2] * v[0])  # zx
    wp.atomic_add(kinetic_tensors, 0, 7, m * v[2] * v[1])  # zy
    wp.atomic_add(kinetic_tensors, 0, 8, m * v[2] * v[2])  # zz


@wp.kernel
def _compute_kinetic_tensor_single_tiled_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    kinetic_tensors: wp.array2d(dtype=Any),
):
    """Compute kinetic contribution to pressure tensor with tile reductions (single system).

    P_kin[i,j] = sum_k m_k * v_k[i] * v_k[j]

    Launch Grid: dim = [num_atoms], block_dim = TILE_DIM
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]

    # Compute local outer product components
    p_xx = m * v[0] * v[0]
    p_xy = m * v[0] * v[1]
    p_xz = m * v[0] * v[2]
    p_yx = m * v[1] * v[0]
    p_yy = m * v[1] * v[1]
    p_yz = m * v[1] * v[2]
    p_zx = m * v[2] * v[0]
    p_zy = m * v[2] * v[1]
    p_zz = m * v[2] * v[2]

    # Convert to tiles for block-level reduction
    t_xx = wp.tile(p_xx)
    t_xy = wp.tile(p_xy)
    t_xz = wp.tile(p_xz)
    t_yx = wp.tile(p_yx)
    t_yy = wp.tile(p_yy)
    t_yz = wp.tile(p_yz)
    t_zx = wp.tile(p_zx)
    t_zy = wp.tile(p_zy)
    t_zz = wp.tile(p_zz)

    # Cooperative sum within block
    s_xx = wp.tile_sum(t_xx)
    s_xy = wp.tile_sum(t_xy)
    s_xz = wp.tile_sum(t_xz)
    s_yx = wp.tile_sum(t_yx)
    s_yy = wp.tile_sum(t_yy)
    s_yz = wp.tile_sum(t_yz)
    s_zx = wp.tile_sum(t_zx)
    s_zy = wp.tile_sum(t_zy)
    s_zz = wp.tile_sum(t_zz)

    # Extract scalar values from tile sums
    sum_xx = s_xx[0]
    sum_xy = s_xy[0]
    sum_xz = s_xz[0]
    sum_yx = s_yx[0]
    sum_yy = s_yy[0]
    sum_yz = s_yz[0]
    sum_zx = s_zx[0]
    sum_zy = s_zy[0]
    sum_zz = s_zz[0]

    # Only first thread in block writes (9 atomics per block)
    if atom_idx % TILE_DIM == 0:
        wp.atomic_add(kinetic_tensors, 0, 0, sum_xx)
        wp.atomic_add(kinetic_tensors, 0, 1, sum_xy)
        wp.atomic_add(kinetic_tensors, 0, 2, sum_xz)
        wp.atomic_add(kinetic_tensors, 0, 3, sum_yx)
        wp.atomic_add(kinetic_tensors, 0, 4, sum_yy)
        wp.atomic_add(kinetic_tensors, 0, 5, sum_yz)
        wp.atomic_add(kinetic_tensors, 0, 6, sum_zx)
        wp.atomic_add(kinetic_tensors, 0, 7, sum_zy)
        wp.atomic_add(kinetic_tensors, 0, 8, sum_zz)


@wp.kernel
def _compute_kinetic_tensor_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    kinetic_tensors: wp.array2d(dtype=Any),
):
    """Compute kinetic contribution to pressure tensor (batched).

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]

    wp.atomic_add(kinetic_tensors, sys_id, 0, m * v[0] * v[0])
    wp.atomic_add(kinetic_tensors, sys_id, 1, m * v[0] * v[1])
    wp.atomic_add(kinetic_tensors, sys_id, 2, m * v[0] * v[2])
    wp.atomic_add(kinetic_tensors, sys_id, 3, m * v[1] * v[0])
    wp.atomic_add(kinetic_tensors, sys_id, 4, m * v[1] * v[1])
    wp.atomic_add(kinetic_tensors, sys_id, 5, m * v[1] * v[2])
    wp.atomic_add(kinetic_tensors, sys_id, 6, m * v[2] * v[0])
    wp.atomic_add(kinetic_tensors, sys_id, 7, m * v[2] * v[1])
    wp.atomic_add(kinetic_tensors, sys_id, 8, m * v[2] * v[2])


@wp.kernel
def _finalize_pressure_tensor_kernel(
    kinetic_tensors: wp.array2d(dtype=Any),
    virial_tensors: wp.array(dtype=Any),
    volumes: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
):
    """Finalize pressure tensor: P = (kinetic + virial) / V.

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    V = volumes[sys_id]
    vir = virial_tensors[sys_id]

    # Compute each component explicitly to avoid type inference issues
    p0 = (kinetic_tensors[sys_id, 0] + vir[0]) / V
    p1 = (kinetic_tensors[sys_id, 1] + vir[1]) / V
    p2 = (kinetic_tensors[sys_id, 2] + vir[2]) / V
    p3 = (kinetic_tensors[sys_id, 3] + vir[3]) / V
    p4 = (kinetic_tensors[sys_id, 4] + vir[4]) / V
    p5 = (kinetic_tensors[sys_id, 5] + vir[5]) / V
    p6 = (kinetic_tensors[sys_id, 6] + vir[6]) / V
    p7 = (kinetic_tensors[sys_id, 7] + vir[7]) / V
    p8 = (kinetic_tensors[sys_id, 8] + vir[8]) / V

    pressure_tensors[sys_id] = type(vir)(p0, p1, p2, p3, p4, p5, p6, p7, p8)


@wp.kernel
def _compute_scalar_pressure_kernel(
    pressure_tensors: wp.array(dtype=Any),
    scalar_pressures: wp.array(dtype=Any),
):
    """Compute scalar pressure: P = (P_xx + P_yy + P_zz) / 3.

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    P = pressure_tensors[sys_id]
    trace = P[0] + P[4] + P[8]
    scalar_pressures[sys_id] = trace / type(trace)(3.0)


# ==============================================================================
# Barostat Energy Kernels
# ==============================================================================


@wp.kernel
def _compute_cell_kinetic_energy_kernel(
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energy: wp.array(dtype=Any),
):
    r"""Compute cell kinetic energy: :math:`KE = 0.5 W \|\dot{\varepsilon}\|_F^2` where ``eps_dot = cell_velocities``.

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    eps_dot = cell_velocities[sys_id]
    W = cell_masses[sys_id]

    ke = type(W)(0.0)
    for i in range(3):
        for j in range(3):
            ke = ke + eps_dot[i, j] * eps_dot[i, j]

    kinetic_energy[sys_id] = type(W)(0.5) * W * ke


@wp.kernel
def _compute_barostat_potential_kernel(
    target_pressures: wp.array(dtype=Any),
    volumes: wp.array(dtype=Any),
    potential_energy: wp.array(dtype=Any),
):
    """Compute barostat potential: U = P_ext * V.

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    potential_energy[sys_id] = P_ext * V


# ==============================================================================
# NPT Velocity Update Kernels
# ==============================================================================


@wp.kernel
def _npt_velocity_half_step_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    eta_dot: wp.array2d(dtype=Any),
    num_atoms: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    r"""NPT isotropic velocity half-step, single system (out-only).

    :math:`v_{new} = v + \frac{dt}{2} \left(\frac{F}{m} - (1 + \frac{d}{N_f}) \dot{\varepsilon} v - \dot{\eta}_1 v\right)`

    where ``eps_dot`` is the isotropic strain rate.

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocity[0]
    eta_dot_1 = eta_dot[0, 0]

    N_f = type(m)(3 * num_atoms[0])
    coupling = type(m)(1.0) + type(m)(3.0) / N_f
    dt_half = dt[0] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_isotropic(h_dot, coupling, eta_dot_1, v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _npt_velocity_half_step_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    eta_dots: wp.array2d(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPT isotropic velocity half-step, batched (out-only).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocities[sys_id]
    eta_dot_1 = eta_dots[sys_id, 0]
    N = num_atoms_per_system[sys_id]

    N_f = type(m)(3 * N)
    coupling = type(m)(1.0) + type(m)(3.0) / N_f
    dt_half = dt[sys_id] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_isotropic(h_dot, coupling, eta_dot_1, v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


# ==============================================================================
# NPH Velocity Update Kernels (no thermostat coupling)
# ==============================================================================


@wp.kernel
def _nph_velocity_half_step_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    num_atoms: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    r"""NPH isotropic velocity half-step, single system (out-only).

    :math:`v_{new} = v + \frac{dt}{2} \left(\frac{F}{m} - (1 + \frac{d}{N_f}) \dot{\varepsilon} v\right)`

    where ``eps_dot`` is the isotropic strain rate.

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocity[0]

    N_f = type(m)(3 * num_atoms[0])
    coupling = type(m)(1.0) + type(m)(3.0) / N_f
    dt_half = dt[0] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_isotropic(h_dot, coupling, type(m)(0.0), v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _nph_velocity_half_step_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPH isotropic velocity half-step, batched (out-only).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocities[sys_id]
    N = num_atoms_per_system[sys_id]

    N_f = type(m)(3 * N)
    coupling = type(m)(1.0) + type(m)(3.0) / N_f
    dt_half = dt[sys_id] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_isotropic(h_dot, coupling, type(m)(0.0), v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


# ==============================================================================
# Position Update Kernels (shared by NPT and NPH)
# ==============================================================================


@wp.kernel
def _position_update_single_kernel(
    positions: wp.array(dtype=Any),
    velocities: wp.array(dtype=Any),
    cell: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    dt: wp.array(dtype=Any),
    positions_out: wp.array(dtype=Any),
):
    r"""Position update for single system (out-only).

    :math:`r_{new} = r + dt \cdot v + dt \cdot \dot{\varepsilon} \cdot r`

    For in-place: host passes same array as positions and positions_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    r = positions[atom_idx]
    v = velocities[atom_idx]
    h_dot = cell_velocity[0]

    positions_out[atom_idx] = _npt_position_step(r, v, h_dot, dt[0])


@wp.kernel
def _position_update_kernel(
    positions: wp.array(dtype=Any),
    velocities: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cells: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    cell_velocities: wp.array(dtype=Any),
    dt: wp.array(dtype=Any),
    positions_out: wp.array(dtype=Any),
):
    """Position update, batched (out-only).

    For in-place: host passes same array as positions and positions_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    r = positions[atom_idx]
    v = velocities[atom_idx]
    h_dot = cell_velocities[sys_id]

    positions_out[atom_idx] = _npt_position_step(r, v, h_dot, dt[sys_id])


# ==============================================================================
# Cell Velocity Update Kernels (Barostat)
# ==============================================================================


@wp.kernel
def _npt_cell_velocity_update_kernel(
    cell_velocities: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
    target_pressures: wp.array(dtype=Any),
    volumes: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
):
    r"""NPT isotropic cell-velocity update: :math:`\dot{\varepsilon} += \frac{dt}{2} \frac{V}{W} \left(\frac{\mathrm{Tr}(P)}{3} + \frac{2 KE}{3NV} - P_{ext}\right)`."""
    sys_id = wp.tid()
    h_dot = cell_velocities[sys_id]
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    W = cell_masses[sys_id]
    KE = kinetic_energies[sys_id]
    N = num_atoms_per_system[sys_id]

    P = pressure_tensors[sys_id]
    P_current = (P[0] + P[4] + P[8]) / type(V)(3.0)

    KE_typed = type(V)(KE)
    N_f = type(V)(3 * N)
    dof_term = type(V)(2.0) * KE_typed / N_f
    P_diff = P_current + dof_term / V - P_ext

    h_dot_accel = V * P_diff / W
    dt_half = dt[sys_id] * type(V)(0.5)
    h_dot_new_diag = h_dot[0, 0] + dt_half * h_dot_accel

    zero = type(V)(0.0)
    cell_velocities[sys_id] = type(h_dot)(
        h_dot_new_diag,
        zero,
        zero,
        zero,
        h_dot_new_diag,
        zero,
        zero,
        zero,
        h_dot_new_diag,
    )


@wp.kernel
def _nph_cell_velocity_update_kernel(
    cell_velocities: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
    target_pressures: wp.array(dtype=Any),
    volumes: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
):
    r"""
    NPH isotropic cell velocity update (no thermostat coupling).

    Algorithm
    ---------
    Updates cell velocity for constant enthalpy (NPH) dynamics. Without
    thermostat coupling, the equation simplifies to:

        :math:`\ddot{h} = \frac{V}{W}(P_{inst} - P_{ext})`

    The system evolves on a constant-enthalpy surface with temperature
    fluctuating naturally.

    Launch Grid
    -----------
    dim = [num_systems]

    Parameters
    ----------
    cell_velocities : wp.array(dtype=mat33f/mat33d)
        Cell velocity matrices. MODIFIED in-place.
    pressure_tensors : wp.array(dtype=vec9f/vec9d)
        Pressure tensors from virial.
    target_pressures : wp.array(dtype=float32/float64)
        Target scalar pressures.
    volumes, cell_masses, kinetic_energies : wp.array
        System properties.
    num_atoms_per_system : wp.array(dtype=wp.int32)
        Atom counts per system.
    dt : wp.array(dtype=float32/float64)
        Time step per system. Shape (B,).

    Notes
    -----
    - No thermostat drag term (:math:`\dot{\eta}_1 \dot{h}`) compared to NPT.
    - Temperature is not controlled; enthalpy H = KE + PE + PV is conserved.
    """
    sys_id = wp.tid()
    h_dot = cell_velocities[sys_id]
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    W = cell_masses[sys_id]
    KE = kinetic_energies[sys_id]
    N = num_atoms_per_system[sys_id]

    # Current pressure from tensor
    P = pressure_tensors[sys_id]
    P_current = (P[0] + P[4] + P[8]) / type(V)(3.0)

    # Cast KE to match V's type
    KE_typed = type(V)(KE)

    # Degrees of freedom contribution
    N_f = type(V)(3 * N)
    dof_term = type(V)(2.0) * KE_typed / N_f

    # Pressure difference
    P_diff = P_current + dof_term / V - P_ext

    # Cell velocity acceleration (isotropic, no thermostat drag)
    h_dot_accel = V * P_diff / W

    zero = type(V)(0.0)
    dt_half = dt[sys_id] * type(V)(0.5)
    h_dot_new_diag = h_dot[0, 0] + dt_half * h_dot_accel

    cell_velocities[sys_id] = type(h_dot)(
        h_dot_new_diag,
        zero,
        zero,
        zero,
        h_dot_new_diag,
        zero,
        zero,
        zero,
        h_dot_new_diag,
    )


# ==============================================================================
# Anisotropic Cell Velocity Update Kernels
# ==============================================================================


@wp.kernel
def _npt_cell_velocity_update_aniso_kernel(
    cell_velocities: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
    target_pressures: wp.array(dtype=Any),  # vec3: [Pxx, Pyy, Pzz]
    volumes: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
):
    r"""NPT anisotropic cell-velocity update: :math:`\dot{\varepsilon}_{ii} += \frac{dt}{2} \frac{V}{W} (P_{ii} - P_{ext,ii})`."""
    sys_id = wp.tid()
    h_dot = cell_velocities[sys_id]
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    W = cell_masses[sys_id]
    KE = kinetic_energies[sys_id]
    N = num_atoms_per_system[sys_id]

    P = pressure_tensors[sys_id]
    P_xx = P[0]
    P_yy = P[4]
    P_zz = P[8]

    KE_typed = type(V)(KE)
    N_f = type(V)(3 * N)
    dof_term = type(V)(2.0) * KE_typed / N_f
    dof_V = dof_term / V

    P_diff_x = P_xx + dof_V - P_ext[0]
    P_diff_y = P_yy + dof_V - P_ext[1]
    P_diff_z = P_zz + dof_V - P_ext[2]

    h_dot_accel_x = V * P_diff_x / W
    h_dot_accel_y = V * P_diff_y / W
    h_dot_accel_z = V * P_diff_z / W

    dt_half = dt[sys_id] * type(V)(0.5)
    h_dot_new_xx = h_dot[0, 0] + dt_half * h_dot_accel_x
    h_dot_new_yy = h_dot[1, 1] + dt_half * h_dot_accel_y
    h_dot_new_zz = h_dot[2, 2] + dt_half * h_dot_accel_z

    zero = type(V)(0.0)
    cell_velocities[sys_id] = type(h_dot)(
        h_dot_new_xx, zero, zero, zero, h_dot_new_yy, zero, zero, zero, h_dot_new_zz
    )


@wp.kernel
def _nph_cell_velocity_update_aniso_kernel(
    cell_velocities: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
    target_pressures: wp.array(dtype=Any),  # vec3: [Pxx, Pyy, Pzz]
    volumes: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
):
    r"""NPH cell velocity update - anisotropic/orthorhombic (no thermostat).

    :math:`\ddot{h}_{ii} = \frac{V}{W}(P_{ii} - P_{ext,ii})`

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    h_dot = cell_velocities[sys_id]
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    W = cell_masses[sys_id]
    KE = kinetic_energies[sys_id]
    N = num_atoms_per_system[sys_id]

    P = pressure_tensors[sys_id]
    P_xx = P[0]
    P_yy = P[4]
    P_zz = P[8]

    KE_typed = type(V)(KE)
    N_f = type(V)(3 * N)
    dof_term = type(V)(2.0) * KE_typed / N_f
    dof_V = dof_term / V

    P_diff_x = P_xx + dof_V - P_ext[0]
    P_diff_y = P_yy + dof_V - P_ext[1]
    P_diff_z = P_zz + dof_V - P_ext[2]

    h_dot_accel_x = V * P_diff_x / W
    h_dot_accel_y = V * P_diff_y / W
    h_dot_accel_z = V * P_diff_z / W

    zero = type(V)(0.0)
    dt_half = dt[sys_id] * type(V)(0.5)
    h_dot_new_xx = h_dot[0, 0] + dt_half * h_dot_accel_x
    h_dot_new_yy = h_dot[1, 1] + dt_half * h_dot_accel_y
    h_dot_new_zz = h_dot[2, 2] + dt_half * h_dot_accel_z

    cell_velocities[sys_id] = type(h_dot)(
        h_dot_new_xx, zero, zero, zero, h_dot_new_yy, zero, zero, zero, h_dot_new_zz
    )


# ==============================================================================
# Triclinic Cell Velocity Update Kernels (Full Stress Tensor Control)
# ==============================================================================


@wp.kernel
def _npt_cell_velocity_update_triclinic_kernel(
    cell_velocities: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
    target_pressures: wp.array(dtype=Any),  # vec9: full stress tensor
    volumes: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
):
    r"""NPT triclinic cell-velocity update: :math:`\dot{\varepsilon}_{ij} += \frac{dt}{2} \frac{V}{W} (P_{ij} - P_{ext,ij})`, DOF correction on diagonal only."""
    sys_id = wp.tid()
    h_dot = cell_velocities[sys_id]
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    W = cell_masses[sys_id]
    KE = kinetic_energies[sys_id]
    N = num_atoms_per_system[sys_id]

    P = pressure_tensors[sys_id]

    KE_typed = type(V)(KE)
    N_f = type(V)(3 * N)
    dof_term = type(V)(2.0) * KE_typed / N_f
    dof_V = dof_term / V

    P_diff_00 = P[0] + dof_V - P_ext[0]
    P_diff_01 = P[1] - P_ext[1]
    P_diff_02 = P[2] - P_ext[2]
    P_diff_10 = P[3] - P_ext[3]
    P_diff_11 = P[4] + dof_V - P_ext[4]
    P_diff_12 = P[5] - P_ext[5]
    P_diff_20 = P[6] - P_ext[6]
    P_diff_21 = P[7] - P_ext[7]
    P_diff_22 = P[8] + dof_V - P_ext[8]

    V_W = V / W
    dt_half = dt[sys_id] * type(V)(0.5)
    h_dot_new_00 = h_dot[0, 0] + dt_half * V_W * P_diff_00
    h_dot_new_01 = h_dot[0, 1] + dt_half * V_W * P_diff_01
    h_dot_new_02 = h_dot[0, 2] + dt_half * V_W * P_diff_02
    h_dot_new_10 = h_dot[1, 0] + dt_half * V_W * P_diff_10
    h_dot_new_11 = h_dot[1, 1] + dt_half * V_W * P_diff_11
    h_dot_new_12 = h_dot[1, 2] + dt_half * V_W * P_diff_12
    h_dot_new_20 = h_dot[2, 0] + dt_half * V_W * P_diff_20
    h_dot_new_21 = h_dot[2, 1] + dt_half * V_W * P_diff_21
    h_dot_new_22 = h_dot[2, 2] + dt_half * V_W * P_diff_22

    cell_velocities[sys_id] = type(h_dot)(
        h_dot_new_00,
        h_dot_new_01,
        h_dot_new_02,
        h_dot_new_10,
        h_dot_new_11,
        h_dot_new_12,
        h_dot_new_20,
        h_dot_new_21,
        h_dot_new_22,
    )


@wp.kernel
def _nph_cell_velocity_update_triclinic_kernel(
    cell_velocities: wp.array(dtype=Any),
    pressure_tensors: wp.array(dtype=Any),
    target_pressures: wp.array(dtype=Any),  # vec9: full stress tensor
    volumes: wp.array(dtype=Any),
    cell_masses: wp.array(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
):
    r"""NPH cell velocity update - full triclinic (no thermostat).

    :math:`\ddot{h}_{ij} = \frac{V}{W}(P_{ij} - P_{ext,ij})`

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    h_dot = cell_velocities[sys_id]
    P_ext = target_pressures[sys_id]
    V = volumes[sys_id]
    W = cell_masses[sys_id]
    KE = kinetic_energies[sys_id]
    N = num_atoms_per_system[sys_id]

    P = pressure_tensors[sys_id]

    KE_typed = type(V)(KE)
    N_f = type(V)(3 * N)
    dof_term = type(V)(2.0) * KE_typed / N_f
    dof_V = dof_term / V

    P_diff_00 = P[0] + dof_V - P_ext[0]
    P_diff_01 = P[1] - P_ext[1]
    P_diff_02 = P[2] - P_ext[2]
    P_diff_10 = P[3] - P_ext[3]
    P_diff_11 = P[4] + dof_V - P_ext[4]
    P_diff_12 = P[5] - P_ext[5]
    P_diff_20 = P[6] - P_ext[6]
    P_diff_21 = P[7] - P_ext[7]
    P_diff_22 = P[8] + dof_V - P_ext[8]

    V_W = V / W
    dt_half = dt[sys_id] * type(V)(0.5)
    h_dot_new_00 = h_dot[0, 0] + dt_half * V_W * P_diff_00
    h_dot_new_01 = h_dot[0, 1] + dt_half * V_W * P_diff_01
    h_dot_new_02 = h_dot[0, 2] + dt_half * V_W * P_diff_02
    h_dot_new_10 = h_dot[1, 0] + dt_half * V_W * P_diff_10
    h_dot_new_11 = h_dot[1, 1] + dt_half * V_W * P_diff_11
    h_dot_new_12 = h_dot[1, 2] + dt_half * V_W * P_diff_12
    h_dot_new_20 = h_dot[2, 0] + dt_half * V_W * P_diff_20
    h_dot_new_21 = h_dot[2, 1] + dt_half * V_W * P_diff_21
    h_dot_new_22 = h_dot[2, 2] + dt_half * V_W * P_diff_22

    cell_velocities[sys_id] = type(h_dot)(
        h_dot_new_00,
        h_dot_new_01,
        h_dot_new_02,
        h_dot_new_10,
        h_dot_new_11,
        h_dot_new_12,
        h_dot_new_20,
        h_dot_new_21,
        h_dot_new_22,
    )


# ==============================================================================
# Anisotropic Velocity Update Kernels
# ==============================================================================


@wp.kernel
def _npt_velocity_half_step_aniso_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    eta_dot: wp.array2d(dtype=Any),
    num_atoms: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPT anisotropic velocity half-step, single system (out-only).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocity[0]
    eta_dot_1 = eta_dot[0, 0]

    N_f = type(m)(3 * num_atoms[0])
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[0] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_anisotropic(h_dot, inv_three_n, eta_dot_1, v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _npt_velocity_half_step_aniso_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    eta_dots: wp.array2d(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPT anisotropic velocity half-step, batched (out-only).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocities[sys_id]
    eta_dot_1 = eta_dots[sys_id, 0]
    N = num_atoms_per_system[sys_id]

    N_f = type(m)(3 * N)
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[sys_id] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_anisotropic(h_dot, inv_three_n, eta_dot_1, v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


# =============================================================================
# Triclinic NPT Velocity Kernels
# =============================================================================


@wp.kernel
def _npt_velocity_half_step_triclinic_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    eta_dot: wp.array2d(dtype=Any),
    num_atoms: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPT triclinic velocity half-step, single system (out-only).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocity[0]
    eta_dot_1 = eta_dot[0, 0]

    N_f = type(m)(3 * num_atoms[0])
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[0] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_triclinic(h_dot, inv_three_n, eta_dot_1, v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _npt_velocity_half_step_triclinic_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    eta_dots: wp.array2d(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPT triclinic velocity half-step, batched (out-only).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocities[sys_id]
    eta_dot_1 = eta_dots[sys_id, 0]
    N = num_atoms_per_system[sys_id]

    N_f = type(m)(3 * N)
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[sys_id] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_triclinic(h_dot, inv_three_n, eta_dot_1, v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


# =============================================================================
# Triclinic NPH Velocity Kernels
# =============================================================================


@wp.kernel
def _nph_velocity_half_step_triclinic_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    num_atoms: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPH triclinic velocity half-step, single system (out-only, no thermostat).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocity[0]

    N_f = type(m)(3 * num_atoms[0])
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[0] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_triclinic(h_dot, inv_three_n, type(m)(0.0), v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _nph_velocity_half_step_triclinic_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPH triclinic velocity half-step, batched (out-only, no thermostat).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocities[sys_id]
    N = num_atoms_per_system[sys_id]

    N_f = type(m)(3 * N)
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[sys_id] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_triclinic(h_dot, inv_three_n, type(m)(0.0), v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _nph_velocity_half_step_aniso_single_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    cell_velocity: wp.array(dtype=Any),
    cell_inv: wp.array(dtype=Any),
    num_atoms: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPH anisotropic velocity half-step, single system (out-only, no thermostat).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocity[0]

    N_f = type(m)(3 * num_atoms[0])
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[0] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_anisotropic(h_dot, inv_three_n, type(m)(0.0), v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


@wp.kernel
def _nph_velocity_half_step_aniso_kernel(
    velocities: wp.array(dtype=Any),
    masses: wp.array(dtype=Any),
    forces: wp.array(dtype=Any),
    batch_idx: wp.array(dtype=wp.int32),
    cell_velocities: wp.array(dtype=Any),
    cells_inv: wp.array(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    dt: wp.array(dtype=Any),
    velocities_out: wp.array(dtype=Any),
):
    """NPH anisotropic velocity half-step, batched (out-only, no thermostat).

    For in-place: host passes same array as velocities and velocities_out.

    Launch Grid: dim = [num_atoms]
    """
    atom_idx = wp.tid()
    sys_id = batch_idx[atom_idx]
    v = velocities[atom_idx]
    m = masses[atom_idx]
    f = forces[atom_idx]
    h_dot = cell_velocities[sys_id]
    N = num_atoms_per_system[sys_id]

    N_f = type(m)(3 * N)
    inv_three_n = type(m)(1.0) / N_f
    dt_half = dt[sys_id] * type(m)(0.5)
    accel = _npt_accel(f, m)
    drag = _drag_anisotropic(h_dot, inv_three_n, type(m)(0.0), v)

    velocities_out[atom_idx] = v + dt_half * (accel - drag)


# ==============================================================================
# Cell Position Update Kernels
# ==============================================================================


@wp.kernel
def _cell_update_kernel(
    cells: wp.array(dtype=Any),
    cell_velocities: wp.array(dtype=Any),
    dt: wp.array(dtype=Any),
    cells_out: wp.array(dtype=Any),
):
    r"""Update cell matrix: :math:`h_{new} = h + dt \cdot \dot{\varepsilon} \cdot h` (out-only, multiplicative).

    For in-place: host passes same array as cells and cells_out.

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    h = cells[sys_id]
    h_dot = cell_velocities[sys_id]
    cells_out[sys_id] = h + dt[sys_id] * wp.mul(h_dot, h)


# ==============================================================================
# NPT Thermostat Kernel
# ==============================================================================


@wp.kernel
def _npt_thermostat_chain_update_kernel(
    eta: wp.array2d(dtype=Any),
    eta_dot: wp.array2d(dtype=Any),
    kinetic_energies: wp.array(dtype=Any),
    target_temps: wp.array(dtype=Any),
    thermostat_masses: wp.array2d(dtype=Any),
    num_atoms_per_system: wp.array(dtype=wp.int32),
    chain_length: wp.int32,
    dt_chain: wp.array(dtype=Any),
):
    """Update Nose-Hoover chain for NPT thermostat.

    Launch Grid: dim = [num_systems]
    """
    sys_id = wp.tid()
    KE = kinetic_energies[sys_id]
    T_target = target_temps[sys_id]
    N = num_atoms_per_system[sys_id]

    # Use eta_dot dtype as reference
    eta_dot_0 = eta_dot[sys_id, 0]

    N_f = type(eta_dot_0)(3 * N)
    kT = type(eta_dot_0)(T_target)
    KE_typed = type(eta_dot_0)(KE)

    # First thermostat driven by kinetic energy difference
    G1 = (type(eta_dot_0)(2.0) * KE_typed - N_f * kT) / thermostat_masses[sys_id, 0]

    dt_chain_sys = dt_chain[sys_id]
    dt_half = dt_chain_sys * type(eta_dot_0)(0.5)
    dt_quarter = dt_chain_sys * type(eta_dot_0)(0.25)

    # Last thermostat
    M_last = chain_length - 1
    eta_dot[sys_id, M_last] = (
        eta_dot[sys_id, M_last]
        + dt_quarter
        * (
            thermostat_masses[sys_id, M_last - 1]
            * eta_dot[sys_id, M_last - 1]
            * eta_dot[sys_id, M_last - 1]
            - kT
        )
        / thermostat_masses[sys_id, M_last]
    )

    # Middle thermostats (reverse order)
    for k in range(M_last - 1, 0, -1):
        eta_dot_k = eta_dot[sys_id, k]
        G_k = (
            thermostat_masses[sys_id, k - 1]
            * eta_dot[sys_id, k - 1]
            * eta_dot[sys_id, k - 1]
            - kT
        ) / thermostat_masses[sys_id, k]
        exp_factor = wp.exp(-dt_quarter * eta_dot[sys_id, k + 1])
        eta_dot[sys_id, k] = eta_dot_k * exp_factor + dt_quarter * G_k

    # First thermostat
    exp_factor = wp.exp(-dt_quarter * eta_dot[sys_id, 1])
    eta_dot[sys_id, 0] = eta_dot[sys_id, 0] * exp_factor + dt_quarter * G1

    # Update positions
    for k in range(chain_length):
        eta[sys_id, k] = eta[sys_id, k] + dt_half * eta_dot[sys_id, k]

    # Second half of velocity updates
    G1_new = (type(eta_dot_0)(2.0) * KE_typed - N_f * kT) / thermostat_masses[sys_id, 0]
    exp_factor = wp.exp(-dt_quarter * eta_dot[sys_id, 1])
    eta_dot[sys_id, 0] = eta_dot[sys_id, 0] * exp_factor + dt_quarter * G1_new

    for k in range(1, M_last):
        G_k = (
            thermostat_masses[sys_id, k - 1]
            * eta_dot[sys_id, k - 1]
            * eta_dot[sys_id, k - 1]
            - kT
        ) / thermostat_masses[sys_id, k]
        exp_factor = wp.exp(-dt_quarter * eta_dot[sys_id, k + 1])
        eta_dot[sys_id, k] = eta_dot[sys_id, k] * exp_factor + dt_quarter * G_k

    # Last thermostat
    G_last = (
        thermostat_masses[sys_id, M_last - 1]
        * eta_dot[sys_id, M_last - 1]
        * eta_dot[sys_id, M_last - 1]
        - kT
    ) / thermostat_masses[sys_id, M_last]
    eta_dot[sys_id, M_last] = eta_dot[sys_id, M_last] + dt_quarter * G_last


# ==============================================================================
# Functional Interfaces - Pressure Calculations
# ==============================================================================


def compute_kinetic_tensor(
    velocities: wp.array,
    masses: wp.array,
    kinetic_tensors: wp.array,
    batch_idx: wp.array = None,
    device: str = None,
) -> wp.array:
    r"""
    Fill ``kinetic_tensors[s]`` with
    :math:`\sum_{i \in s} m_i (\mathbf{v}_i \otimes \mathbf{v}_i)` (vec9 layout)
    — the same accumulation ``compute_pressure_tensor`` performs internally,
    exposed standalone.

    Lets a caller compute the kinetic tensor separately, optionally
    post-process it, and feed it back via
    ``compute_pressure_tensor(..., compute_kinetic=False)``. Using this kernel
    for the reduction gives machine-precision parity with the value
    ``compute_pressure_tensor`` would otherwise compute itself.

    Parameters
    ----------
    velocities : wp.array(dtype=wp.vec3f or wp.vec3d)
        Particle velocities. Shape (N,).
    masses : wp.array
        Particle masses. Shape (N,).
    kinetic_tensors : wp.array(dtype=scalar, ndim=2)
        Output kinetic tensor. Shape (B, 9). Zeroed internally before
        accumulation. MODIFIED in-place.
    batch_idx : wp.array(dtype=wp.int32), optional
        System index for each atom. If None, assumes single system.
    device : str, optional
        Warp device.

    Returns
    -------
    wp.array
        The ``kinetic_tensors`` array, filled with
        :math:`\sum_i m_i (\mathbf{v}_i \otimes \mathbf{v}_i)` per system.
    """
    if device is None:
        device = velocities.device

    num_atoms = velocities.shape[0]

    kinetic_tensors.zero_()
    if batch_idx is None:
        kernel = _KINETIC_TENSOR_FAMILY.single
        inputs = [velocities, masses, kinetic_tensors]
    else:
        kernel = _KINETIC_TENSOR_FAMILY.batch_idx
        inputs = [velocities, masses, batch_idx, kinetic_tensors]
    wp.launch(kernel, dim=num_atoms, inputs=inputs, device=device, block_dim=TILE_DIM)

    return kinetic_tensors


[docs] def compute_pressure_tensor( velocities: wp.array, masses: wp.array, virial_tensors: wp.array, cells: wp.array, kinetic_tensors: wp.array, pressure_tensors: wp.array, volumes: wp.array, batch_idx: wp.array = None, device: str = None, compute_kinetic: bool = True, ) -> wp.array: r""" Compute full pressure tensor :math:`\mathbf{P} = (\mathbf{K} + \mathbf{W}) / V`, with kinetic term :math:`\mathbf{K}`, virial term :math:`\mathbf{W}`, and cell volume :math:`V`. Parameters ---------- velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. Shape (N,). masses : wp.array Particle masses. Shape (N,). virial_tensors : wp.array(dtype=vec9f or vec9d) Virial tensor from forces (physics sign). Shape (B,). cells : wp.array(dtype=wp.mat33f or wp.mat33d) Cell matrices. Shape (B,). kinetic_tensors : wp.array(dtype=scalar, ndim=2) Kinetic tensor :math:`\mathbf{K}_s = \sum_{i \in s} m_i (\mathbf{v}_i \otimes \mathbf{v}_i)`, vec9 layout. Shape (B, 9). When compute_kinetic is True (default) this is a scratch array zeroed and filled internally from velocities/masses. When compute_kinetic is False it is an INPUT supplying a caller-precomputed kinetic tensor; it is not zeroed or recomputed. pressure_tensors : wp.array(dtype=vec9f or vec9d) Output pressure tensor. Shape (B,). volumes : wp.array(dtype=scalar) Pre-computed cell volumes. Shape (B,). Caller must pre-compute via compute_cell_volume. batch_idx : wp.array(dtype=wp.int32), optional System index for each atom. If None, assumes single system. device : str, optional Warp device. compute_kinetic : bool, optional If True (default), recompute the kinetic tensor from velocities/masses internally (byte-identical to legacy behavior). If False, use the caller-supplied value already in ``kinetic_tensors`` instead of recomputing it. See ``compute_kinetic_tensor`` for the matching reduction helper. Returns ------- wp.array(dtype=vec9f or vec9d) Pressure tensor. Shape (B,). """ if device is None: device = velocities.device num_atoms = velocities.shape[0] num_systems = cells.shape[0] if compute_kinetic: kinetic_tensors.zero_() if batch_idx is None: kernel = _KINETIC_TENSOR_FAMILY.single inputs = [velocities, masses, kinetic_tensors] else: kernel = _KINETIC_TENSOR_FAMILY.batch_idx inputs = [velocities, masses, batch_idx, kinetic_tensors] wp.launch( kernel, dim=num_atoms, inputs=inputs, device=device, block_dim=TILE_DIM ) wp.launch( _finalize_pressure_tensor_kernel, dim=num_systems, inputs=[kinetic_tensors, virial_tensors, volumes, pressure_tensors], device=device, ) return pressure_tensors
[docs] def compute_scalar_pressure( pressure_tensors: wp.array, scalar_pressures: wp.array, device: str = None, ) -> wp.array: """ Compute scalar pressure from pressure tensor. .. math:: P = (P_{xx} + P_{yy} + P_{zz}) / 3 Parameters ---------- pressure_tensors : wp.array(dtype=vec9f or vec9d) Pressure tensor. Shape (B,). scalar_pressures : wp.array(dtype=scalar) Output scalar pressure. Shape (B,). device : str, optional Warp device. Returns ------- wp.array Scalar pressure. Shape (B,). """ if device is None: device = pressure_tensors.device num_systems = pressure_tensors.shape[0] wp.launch( _compute_scalar_pressure_kernel, dim=num_systems, inputs=[pressure_tensors, scalar_pressures], device=device, ) return scalar_pressures
# ============================================================================== # Array Broadcast Kernels (Pure Warp - no numpy) # ============================================================================== @wp.kernel def _broadcast_scalar_f32_kernel( src: wp.array(dtype=wp.float32), dst: wp.array(dtype=wp.float32), ): """Broadcast scalar from src[0] to all elements of dst. Launch Grid: dim = [dst.shape[0]] """ idx = wp.tid() dst[idx] = src[0] @wp.kernel def _broadcast_scalar_f64_kernel( src: wp.array(dtype=wp.float64), dst: wp.array(dtype=wp.float64), ): """Broadcast scalar from src[0] to all elements of dst. Launch Grid: dim = [dst.shape[0]] """ idx = wp.tid() dst[idx] = src[0] @wp.kernel def _broadcast_scalar_i32_kernel( src: wp.array(dtype=wp.int32), dst: wp.array(dtype=wp.int32), ): """Broadcast scalar from src[0] to all elements of dst. Launch Grid: dim = [dst.shape[0]] """ idx = wp.tid() dst[idx] = src[0] # ============================================================================== # Barostat Mass Kernel # ============================================================================== @wp.kernel def _compute_barostat_mass_kernel( temperatures: wp.array(dtype=Any), tau_p: wp.array(dtype=Any), num_atoms: wp.array(dtype=wp.int32), masses_out: wp.array(dtype=Any), ): r""" Compute barostat masses for NPT/NPH simulations. Algorithm --------- The barostat mass is computed from the MTK equations: :math:`W = (N_f + d) k_B T \tau_p^2` where N_f = 3N is degrees of freedom, d = 3 is dimensionality. Launch Grid ----------- dim = [num_systems] Parameters ---------- temperatures : wp.array(dtype=float32/float64) Target temperatures per system. Shape (B,). tau_p : wp.array(dtype=float32/float64) Pressure relaxation times per system. Shape (B,). num_atoms : wp.array(dtype=wp.int32) Atom counts per system. Shape (B,). masses_out : wp.array(dtype=float32/float64) Output barostat masses. Shape (B,). Notes ----- - Larger W = slower pressure equilibration (more stable) - Smaller W = faster equilibration (may cause oscillations) """ sys_id = wp.tid() T = temperatures[sys_id] tau = tau_p[sys_id] N = num_atoms[sys_id] # N_f = 3N degrees of freedom, d = 3 dimensionality N_f = type(T)(3 * N) d = type(T)(3.0) # W = (N_f + d) * kT * τ_p² masses_out[sys_id] = (N_f + d) * T * tau * tau # ============================================================================== # Kernel Family Tables # ============================================================================== # -- Kinetic tensor: single vs batched (tiled kernels, needs block_dim) ------ # Batched uses the per-atom atomic kernel, not the tiled one: a tile block can # span multiple systems, and the block-wide tile_sum would misattribute the # whole block's contribution to the first thread's system. _KINETIC_TENSOR_FAMILY = KernelFamily( single=_compute_kinetic_tensor_single_tiled_kernel, batch_idx=_compute_kinetic_tensor_kernel, ) # -- Position update: single vs batched (shared by NPT and NPH) ------------- _POSITION_UPDATE_FAMILY = KernelFamily( single=_position_update_single_kernel, batch_idx=_position_update_kernel, ) # -- NPT velocity half-step: mode -> KernelFamily (single/batch) ------------ _NPT_VELOCITY_FAMILIES = { "isotropic": KernelFamily( single=_npt_velocity_half_step_single_kernel, batch_idx=_npt_velocity_half_step_kernel, ), "anisotropic": KernelFamily( single=_npt_velocity_half_step_aniso_single_kernel, batch_idx=_npt_velocity_half_step_aniso_kernel, ), "triclinic": KernelFamily( single=_npt_velocity_half_step_triclinic_single_kernel, batch_idx=_npt_velocity_half_step_triclinic_kernel, ), } # -- NPH velocity half-step: mode -> KernelFamily (single/batch) ------------ _NPH_VELOCITY_FAMILIES = { "isotropic": KernelFamily( single=_nph_velocity_half_step_single_kernel, batch_idx=_nph_velocity_half_step_kernel, ), "anisotropic": KernelFamily( single=_nph_velocity_half_step_aniso_single_kernel, batch_idx=_nph_velocity_half_step_aniso_kernel, ), "triclinic": KernelFamily( single=_nph_velocity_half_step_triclinic_single_kernel, batch_idx=_nph_velocity_half_step_triclinic_kernel, ), } # -- Barostat kernels: target_pressures.dtype -> kernel ---------------------- _NPT_BAROSTAT_KERNELS = { wp.float32: _npt_cell_velocity_update_kernel, wp.float64: _npt_cell_velocity_update_kernel, wp.vec3f: _npt_cell_velocity_update_aniso_kernel, wp.vec3d: _npt_cell_velocity_update_aniso_kernel, vec9f: _npt_cell_velocity_update_triclinic_kernel, vec9d: _npt_cell_velocity_update_triclinic_kernel, } _NPH_BAROSTAT_KERNELS = { wp.float32: _nph_cell_velocity_update_kernel, wp.float64: _nph_cell_velocity_update_kernel, wp.vec3f: _nph_cell_velocity_update_aniso_kernel, wp.vec3d: _nph_cell_velocity_update_aniso_kernel, vec9f: _nph_cell_velocity_update_triclinic_kernel, vec9d: _nph_cell_velocity_update_triclinic_kernel, } # ============================================================================== # Functional Interfaces - Barostat Utilities # ==============================================================================
[docs] def compute_barostat_mass( target_temperature: wp.array, tau_p: wp.array, num_atoms: wp.array, masses_out: wp.array, device: str = None, ) -> wp.array: r""" Compute barostat mass(es) for desired pressure fluctuation timescale. The barostat mass determines the inertia of cell volume/shape fluctuations in NPT/NPH simulations. It is computed from the Martyna-Tobias-Klein equations [MTK1994]_ to give a characteristic pressure relaxation time :math:`\tau_p`: .. math:: W = (N_f + d) \cdot k_B T \cdot \tau_p^2 where: - N_f = 3N is the number of degrees of freedom (N atoms in 3D) - d = 3 is the dimensionality (for 3D simulations) - k_B T is the thermal energy (in reduced units, k_B = 1) - :math:`\tau_p` is the pressure relaxation time Larger W gives slower pressure equilibration (more stable but slower to reach target pressure). Smaller W gives faster equilibration but may cause oscillations. Parameters ---------- target_temperature : wp.array Per-system target temperatures. Shape (num_systems,). Caller must pre-broadcast if a single value applies to all systems. tau_p : wp.array Per-system pressure relaxation times. Shape (num_systems,). Typical values: 0.5-2.0 ps. Caller must pre-broadcast if a single value applies to all systems. num_atoms : wp.array(dtype=wp.int32) Per-system atom counts. Shape (num_systems,). Caller must pre-broadcast if a single value applies to all systems. masses_out : wp.array Output barostat masses W. Shape (num_systems,). device : str, optional Warp device. Returns ------- wp.array Barostat mass(es) W. Shape (num_systems,). Examples -------- Batched systems (using wp.arrays): >>> temps = wp.array([1.0, 2.0], dtype=wp.float64, device="cuda:0") >>> tau = wp.array([1.0, 1.0], dtype=wp.float64, device="cuda:0") >>> n_atoms = wp.array([100, 200], dtype=wp.int32, device="cuda:0") >>> W = wp.empty(2, dtype=wp.float64, device="cuda:0") >>> compute_barostat_mass(temps, tau, n_atoms, W) >>> print(W.numpy()) # [303.0, 1206.0] Notes ----- - The formula assumes k_B = 1 (reduced units). Scale :math:`\tau_p` accordingly for real units. - For isotropic barostat, a single mass controls all cell dimensions. - For anisotropic/triclinic barostat [SSM2004]_, the same mass is typically used for all cell velocity components. - All input arrays must have the same length (num_systems). The caller is responsible for broadcasting scalar values to arrays before calling. References ---------- .. [MTK1994] Martyna, Tobias, Klein, J. Chem. Phys. 101, 4177 (1994) .. [SSM2004] Shinoda, Shiga, Mikami, Phys. Rev. B 69, 134103 (2004) """ if device is None: device = target_temperature.device num_systems = target_temperature.shape[0] # Launch kernel wp.launch( _compute_barostat_mass_kernel, dim=num_systems, inputs=[target_temperature, tau_p, num_atoms, masses_out], device=device, ) return masses_out
[docs] def compute_cell_kinetic_energy( cell_velocities: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, cells_inv: wp.array = None, volumes: wp.array = None, device: str = None, ) -> wp.array: r""" Compute kinetic energy of cell degrees of freedom. :math:`\mathrm{KE}_{\text{cell}} = \frac{1}{2} W \|\dot{\varepsilon}\|_F^2` Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon} = p_g/W`. Shape (B,). cell_masses : wp.array Barostat masses. Shape (B,). Note that this is the per-DOF mass :math:`W = (d + 1) k_B T \tau_p^2` consumed by the strain-rate update, not the full barostat mass :math:`d \cdot W`. kinetic_energy : wp.array(dtype=scalar) Output cell kinetic energy. Shape (B,). cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. volumes : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. device : str, optional Warp device. Returns ------- wp.array Cell kinetic energy. Shape (B,). """ if cells_inv is not None: _warn_cells_inv_deprecated() if volumes is not None: _warn_volumes_deprecated() volumes = None if device is None: device = cell_velocities.device h_inv = _ensure_cells_inv(cells_inv, volumes, cell_velocities, device) num_systems = cell_velocities.shape[0] wp.launch( _compute_cell_kinetic_energy_kernel, dim=num_systems, inputs=[cell_velocities, h_inv, cell_masses, kinetic_energy], device=device, ) return kinetic_energy
[docs] def compute_barostat_potential_energy( target_pressures: wp.array, volumes: wp.array, potential_energy: wp.array, device: str = None, ) -> wp.array: r""" Compute barostat potential energy. .. math:: U = P_{\text{ext}} V where :math:`P_{\text{ext}}` is the target pressure and :math:`V` the cell volume. Parameters ---------- target_pressures : wp.array External/target pressures. Shape (B,). volumes : wp.array Cell volumes. Shape (B,). potential_energy : wp.array Output barostat potential energy. Shape (B,). device : str, optional Warp device. Returns ------- wp.array Barostat potential energy. Shape (B,). """ if device is None: device = target_pressures.device num_systems = target_pressures.shape[0] wp.launch( _compute_barostat_potential_kernel, dim=num_systems, inputs=[target_pressures, volumes, potential_energy], device=device, ) return potential_energy
# ============================================================================== # Functional Interfaces - NPT Integration # ==============================================================================
[docs] def npt_thermostat_half_step( eta: wp.array, eta_dot: wp.array, kinetic_energy: wp.array, target_temperature: wp.array, thermostat_masses: wp.array, num_atoms_per_system: wp.array, chain_length: int, dt: wp.array, device: str = None, ) -> None: r""" Perform Nose-Hoover chain thermostat half-step for NPT. Propagates the thermostat chain with the leading driving force .. math:: G_1 = \frac{2\,\mathrm{KE} - N_f\,k_B T}{Q_1}, \qquad G_k = \frac{Q_{k-1}\,\dot{\eta}_{k-1}^2 - k_B T}{Q_k} and advances the chain positions by :math:`\eta_k \mathrel{+}= \tfrac{\Delta t}{2}\,\dot{\eta}_k`, where :math:`N_f = 3N`, :math:`Q_k` are the ``thermostat_masses`` and :math:`\dot{\eta}_k` the chain velocities. Parameters ---------- eta : wp.array2d Thermostat positions. Shape (B, chain_length). MODIFIED in-place. eta_dot : wp.array2d Thermostat velocities. Shape (B, chain_length). MODIFIED in-place. kinetic_energy : wp.array Kinetic energy per system. Shape (B,). target_temperature : wp.array Target temperatures. Shape (B,). thermostat_masses : wp.array2d Thermostat masses. Shape (B, chain_length). num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). chain_length : int Number of thermostats in chain. dt : wp.array Full time step dt per system. Shape (B,). The half-step and quarter-step factors are applied internally. device : str, optional Warp device. """ if device is None: device = eta.device num_systems = eta.shape[0] wp.launch( _npt_thermostat_chain_update_kernel, dim=num_systems, inputs=[ eta, eta_dot, kinetic_energy, target_temperature, thermostat_masses, num_atoms_per_system, chain_length, dt, ], device=device, )
[docs] def npt_barostat_half_step( cell_velocities: wp.array, pressure_tensors: wp.array, target_pressures: wp.array, volumes: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, num_atoms_per_system: wp.array, dt: wp.array, device: str = None, ) -> None: r"""NPT barostat half-step: :math:`\dot{\varepsilon} += \frac{\Delta t}{2} \frac{V}{W} (P_{\text{inst}} - P_{\text{ext}})`. Mode is dispatched by ``target_pressures`` dtype (scalar / vec3 / vec9). Barostat-NHC coupling is applied separately by the caller. Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). MODIFIED in-place. pressure_tensors : wp.array(dtype=vec9f or vec9d) Current pressure tensors from virial. Shape (B,). target_pressures : wp.array External pressure(s); dtype selects mode (scalar/vec3/vec9). volumes, cell_masses, kinetic_energy : wp.array(dtype=scalar) Cell volumes V, barostat masses W, system KE. Shape (B,). num_atoms_per_system : wp.array(dtype=wp.int32) Atoms per system. Shape (B,). dt : wp.array(dtype=scalar) Full time step; half-step factor applied internally. device : str, optional Warp device. Default: inferred from ``cell_velocities``. See Also -------- nph_barostat_half_step : NPH (no thermostat). npt_velocity_half_step : Particle velocity update with NHC drag. References ---------- See ``compute_barostat_mass`` for the MTK1994 and SSM2004 citations. """ if device is None: device = cell_velocities.device num_systems = cell_velocities.shape[0] tp_dtype = target_pressures.dtype kernel = _NPT_BAROSTAT_KERNELS.get(tp_dtype) if kernel is None: raise ValueError( f"Unsupported target_pressures dtype: {tp_dtype}. " f"Expected scalar (float32/float64) for isotropic, " f"vec3 for anisotropic, or vec9 for triclinic." ) wp.launch( kernel, dim=num_systems, inputs=[ cell_velocities, pressure_tensors, target_pressures, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, ], device=device, )
# Keep separate functions for backwards compatibility and explicit control def npt_barostat_half_step_aniso( cell_velocities: wp.array, pressure_tensors: wp.array, target_pressures: wp.array, volumes: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, num_atoms_per_system: wp.array, dt: wp.array, device: str = None, ) -> None: r"""Anisotropic NPT barostat half-step with independent x, y, z pressure control. Updates the diagonal strain-rate components .. math:: \dot{\varepsilon}_{ii} \mathrel{+}= \frac{\Delta t}{2}\,\frac{V}{W} \left(P_{ii} + \frac{2\,\mathrm{KE}}{3 N V} - P_{\text{ext},ii}\right) Convenience alias for :func:`npt_barostat_half_step` with ``vec3`` target pressures. Prefer calling :func:`npt_barostat_half_step` directly; this function exists for backwards compatibility. Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). Modified in-place. pressure_tensors : wp.array(dtype=vec9f or vec9d) Current pressure tensors from virial. Shape (B,). target_pressures : wp.array(dtype=wp.vec3f or wp.vec3d) Target pressures [Pxx, Pyy, Pzz] per system. Shape (B,). volumes : wp.array(dtype=scalar) Cell volumes V. Shape (B,). cell_masses : wp.array(dtype=scalar) Barostat masses W. Shape (B,). kinetic_energy : wp.array(dtype=scalar) System kinetic energies. Shape (B,). num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system; half-step factor applied internally. Shape (B,). device : str, optional Warp device. Default: inferred from ``cell_velocities``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.npt_barostat_half_step` : Unified dispatcher for all modes. """ if device is None: device = cell_velocities.device num_systems = cell_velocities.shape[0] wp.launch( _npt_cell_velocity_update_aniso_kernel, dim=num_systems, inputs=[ cell_velocities, pressure_tensors, target_pressures, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, ], device=device, ) def npt_barostat_half_step_triclinic( cell_velocities: wp.array, pressure_tensors: wp.array, target_pressures: wp.array, volumes: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, num_atoms_per_system: wp.array, dt: wp.array, device: str = None, ) -> None: r"""Triclinic NPT barostat half-step with full stress tensor control. Updates all nine strain-rate components .. math:: \dot{\varepsilon}_{ij} \mathrel{+}= \frac{\Delta t}{2}\,\frac{V}{W} \left(P_{ij} - P_{\text{ext},ij}\right) with the kinetic degrees-of-freedom correction :math:`+\,\tfrac{2 \mathrm{KE}}{3 N V}` added to the diagonal terms only. Convenience alias for :func:`npt_barostat_half_step` with ``vec9`` target pressures. Prefer calling :func:`npt_barostat_half_step` directly; this function exists for backwards compatibility. Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). Modified in-place. pressure_tensors : wp.array(dtype=vec9f or vec9d) Current pressure tensors from virial. Shape (B,). target_pressures : wp.array(dtype=vec9f or vec9d) Full target stress tensors [xx, xy, xz, yx, yy, yz, zx, zy, zz] per system. Shape (B,). volumes : wp.array(dtype=scalar) Cell volumes V. Shape (B,). cell_masses : wp.array(dtype=scalar) Barostat masses W. Shape (B,). kinetic_energy : wp.array(dtype=scalar) System kinetic energies. Shape (B,). num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system; half-step factor applied internally. Shape (B,). device : str, optional Warp device. Default: inferred from ``cell_velocities``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.npt_barostat_half_step` : Unified dispatcher for all modes. """ if device is None: device = cell_velocities.device num_systems = cell_velocities.shape[0] wp.launch( _npt_cell_velocity_update_triclinic_kernel, dim=num_systems, inputs=[ cell_velocities, pressure_tensors, target_pressures, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, ], device=device, )
[docs] def npt_velocity_half_step( velocities: wp.array, masses: wp.array, forces: wp.array, cell_velocities: wp.array, # NOTE: ``volumes``, ``eta_dots``, ``num_atoms``, and ``dt`` default to # ``None`` only so legacy callers that still pass ``volumes`` positionally # keep working while the deprecation is staged. Once ``volumes`` is # removed (next minor cycle) these can become required positional # arguments again. volumes: wp.array = None, eta_dots: wp.array = None, num_atoms: wp.array = None, dt: wp.array = None, batch_idx: wp.array = None, num_atoms_per_system: wp.array = None, cells_inv: wp.array = None, mode: str = "isotropic", device: str = None, ) -> None: r""" Perform half-step velocity update for NPT ensemble. Updates particle velocities accounting for: 1. Forces from the potential energy surface 2. Coupling to barostat (cell velocity / strain rate) 3. Coupling to thermostat (Nose-Hoover chain) Mathematical Formulation ------------------------ **Isotropic mode** (default) :math:`\mathbf{v} \leftarrow \mathbf{v} \exp\!\left(-\Delta t \left((1 + 1/N_{\text{atoms}}) \frac{\operatorname{Tr}(\dot{\varepsilon})}{3} + \dot{\eta}_1\right)\right)` **Anisotropic / triclinic mode** :math:`\mathbf{v} \leftarrow \mathbf{v} \exp\!\left(-\Delta t \left(\dot{\varepsilon} + \frac{\operatorname{Tr}(\dot{\varepsilon})}{3 N_{\text{atoms}}} I + \dot{\eta}_1 I\right)\right)` where ``eps_dot = cell_velocities = p_g/W`` and :math:`\dot{\eta}_1` is the first thermostat-chain velocity. This primitive already applies particle thermostat drag through ``eta_dots[:, 0]``; callers that apply particle NHC velocity scaling as a separate Trotter operator should use :func:`nph_velocity_half_step` or another no-thermostat velocity primitive instead. Parameters ---------- velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. Shape (N,). **MODIFIED in-place.** masses : wp.array(dtype=scalar) Particle masses. Shape (N,). forces : wp.array(dtype=wp.vec3f or wp.vec3d) Forces on particles. Shape (N,). cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon} = p_g/W`. Shape (B,). volumes : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. eta_dots : wp.array2d(dtype=scalar) Thermostat chain velocities. Shape (B, chain_length). num_atoms : wp.array(dtype=wp.int32) Atom count for single-system mode. Shape (1,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). The half-step factor is applied internally. batch_idx : wp.array(dtype=wp.int32), optional System index for each atom. Required for batched simulations. num_atoms_per_system : wp.array(dtype=wp.int32), optional Number of atoms per system. Required for batched simulations. cells_inv : wp.array(dtype=wp.mat33f or wp.mat33d), optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. mode : str, optional Pressure control mode. One of: - ``"isotropic"`` (default): scalar coupling on :math:`\operatorname{Tr}(\dot{\varepsilon})/3` - ``"anisotropic"``: diagonal coupling on :math:`\dot{\varepsilon}` - ``"triclinic"``: full-tensor coupling on :math:`\dot{\varepsilon}` device : str, optional Warp device. Examples -------- Single system (isotropic): >>> npt_velocity_half_step( ... velocities, masses, forces, cell_velocities, ... eta_dots=eta_dots, num_atoms=num_atoms, dt=dt, ... ) See Also -------- npt_barostat_half_step : Cell velocity update step. npt_position_update : Position update step. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if volumes is not None: _warn_volumes_deprecated() volumes = None if eta_dots is None or num_atoms is None or dt is None: raise ValueError( "npt_velocity_half_step requires 'eta_dots', 'num_atoms', and 'dt'." ) # In-place: delegate to _out with velocities as both input and output npt_velocity_half_step_out( velocities, masses, forces, cell_velocities, volumes, eta_dots, num_atoms, dt, velocities_out=velocities, batch_idx=batch_idx, num_atoms_per_system=num_atoms_per_system, cells_inv=cells_inv, mode=mode, device=device, _skip_validation=True, )
[docs] def npt_velocity_half_step_out( velocities: wp.array, masses: wp.array, forces: wp.array, cell_velocities: wp.array, # NOTE: ``volumes``, ``eta_dots``, ``num_atoms``, ``dt``, and # ``velocities_out`` default to ``None`` only to keep legacy positional # callers working while ``volumes`` is being deprecated. Once ``volumes`` # is removed they can become required positional arguments again. volumes: wp.array = None, eta_dots: wp.array = None, num_atoms: wp.array = None, dt: wp.array = None, velocities_out: wp.array = None, batch_idx: wp.array = None, num_atoms_per_system: wp.array = None, cells_inv: wp.array = None, mode: str = "isotropic", device: str = None, _skip_validation: bool = False, ) -> wp.array: r""" Perform half-step velocity update for NPT (non-mutating). Applies the same update as :func:`npt_velocity_half_step`, .. math:: \mathbf{v}(t + \tfrac{\Delta t}{2}) = \mathbf{v} + \frac{\Delta t}{2}\left(\frac{\mathbf{F}}{m} - \mathbf{D}(\dot{\varepsilon}, \dot{\eta}_1)\,\mathbf{v}\right) where :math:`\mathbf{D}` is the mode-dependent barostat + thermostat drag (see :func:`npt_velocity_half_step`), but writes into ``velocities_out`` instead of mutating ``velocities``. Like the in-place variant, this includes particle thermostat drag through ``eta_dots[:, 0]`` and should not be combined with separate particle NHC velocity scaling. Parameters ---------- velocities : wp.array Input velocities (not modified when velocities_out differs). masses, forces, cell_velocities : wp.array System state arrays. volumes : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. eta_dots : wp.array Thermostat chain velocities. num_atoms : wp.array(dtype=wp.int32) Atom count for single-system mode. Shape (1,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). The half-step factor is applied internally. velocities_out : wp.array Pre-allocated output array. batch_idx : wp.array, optional System indices for batched simulations. num_atoms_per_system : wp.array, optional Atom counts per system. cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. mode : str, optional Pressure control mode: "isotropic", "anisotropic", or "triclinic". device : str, optional Warp device. Returns ------- wp.array Updated velocities. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if volumes is not None: _warn_volumes_deprecated() volumes = None if eta_dots is None or num_atoms is None or dt is None or velocities_out is None: raise ValueError( "npt_velocity_half_step_out requires 'eta_dots', 'num_atoms', " "'dt', and 'velocities_out'." ) if device is None: device = velocities.device if not _skip_validation: validate_out_array(velocities_out, velocities, "velocities_out") h_inv = _ensure_cells_inv(cells_inv, volumes, cell_velocities, device) exec_mode = resolve_execution_mode(batch_idx, None) n_atoms = velocities.shape[0] if mode not in _NPT_VELOCITY_FAMILIES: raise ValueError( f"Unknown mode: '{mode}'. Expected 'isotropic', 'anisotropic', or 'triclinic'." ) family = _NPT_VELOCITY_FAMILIES[mode] launch_family( family, mode=exec_mode, dim=n_atoms, inputs_single=[ velocities, masses, forces, cell_velocities, h_inv, eta_dots, num_atoms, dt, velocities_out, ], inputs_batch=[ velocities, masses, forces, batch_idx, cell_velocities, h_inv, eta_dots, num_atoms_per_system, dt, velocities_out, ], device=device, ) return velocities_out
[docs] def npt_position_update( positions: wp.array, velocities: wp.array, cells: wp.array, cell_velocities: wp.array, dt: wp.array, cells_inv: wp.array = None, batch_idx: wp.array = None, device: str = None, ) -> None: r"""Update particle positions for NPT integration in-place. Applies :math:`\mathbf{r} \leftarrow \mathbf{r} + \Delta t\,(\mathbf{v} + \dot{\varepsilon}\,\mathbf{r})` where ``cell_velocities`` is the strain-rate tensor :math:`\dot{\varepsilon} = p_g / W`. Parameters ---------- positions : wp.array(dtype=wp.vec3f or wp.vec3d) Particle positions. Shape (N,). Modified in-place. velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. Shape (N,). cells : wp.array(dtype=wp.mat33f or wp.mat33d) Cell matrices. Shape (B,). cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. batch_idx : wp.array(dtype=wp.int32), optional System index for each atom. Required for batched simulations. device : str, optional Warp device. Default: inferred from ``positions``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.npt_position_update_out` : Non-mutating variant. :func:`nvalchemiops.dynamics.integrators.npt.npt_cell_update` : Cell matrix update step. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None # In-place: delegate to _out with positions as both input and output npt_position_update_out( positions, velocities, cells, cell_velocities, dt, positions, cells_inv=cells_inv, batch_idx=batch_idx, device=device, _skip_validation=True, )
[docs] def npt_position_update_out( positions: wp.array, velocities: wp.array, cells: wp.array, cell_velocities: wp.array, dt: wp.array, positions_out: wp.array, cells_inv: wp.array = None, batch_idx: wp.array = None, device: str = None, _skip_validation: bool = False, ) -> wp.array: r"""Update particle positions for NPT integration, writing results into a pre-allocated output. Non-mutating variant of :func:`npt_position_update`, applying the scaled-coordinate position update .. math:: \mathbf{r}(t + \Delta t) = \mathbf{r} + \Delta t\,(\mathbf{v} + \dot{\varepsilon}\,\mathbf{r}) into ``positions_out``. Parameters ---------- positions : wp.array(dtype=wp.vec3f or wp.vec3d) Input particle positions. Shape (N,). Not modified when ``positions_out`` differs. velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. Shape (N,). cells : wp.array(dtype=wp.mat33f or wp.mat33d) Cell matrices. Shape (B,). cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). positions_out : wp.array(dtype=wp.vec3f or wp.vec3d) Pre-allocated output array for updated positions. Shape (N,). cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. batch_idx : wp.array(dtype=wp.int32), optional System index for each atom. Required for batched simulations. device : str, optional Warp device. Default: inferred from ``positions``. Returns ------- wp.array(dtype=wp.vec3f or wp.vec3d) Updated positions array (same object as ``positions_out``). Shape (N,). See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.npt_position_update` : In-place variant. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if device is None: device = positions.device if not _skip_validation: validate_out_array(positions_out, positions, "positions_out") cells_inv = _ensure_cells_inv(cells_inv, None, cell_velocities, device) exec_mode = resolve_execution_mode(batch_idx, None) num_atoms = positions.shape[0] launch_family( _POSITION_UPDATE_FAMILY, mode=exec_mode, dim=num_atoms, inputs_single=[ positions, velocities, cells, cells_inv, cell_velocities, dt, positions_out, ], inputs_batch=[ positions, velocities, batch_idx, cells, cells_inv, cell_velocities, dt, positions_out, ], device=device, ) return positions_out
[docs] def npt_cell_update( cells: wp.array, cell_velocities: wp.array, dt: wp.array, device: str = None, ) -> None: r""" Update cell matrices: :math:`\mathbf{h}_{\text{new}} = \mathbf{h} + \Delta t\,\dot{\varepsilon}\,\mathbf{h}`. Parameters ---------- cells : wp.array Cell matrices. MODIFIED in-place. cell_velocities : wp.array Cell velocity matrices. dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). device : str, optional Warp device. """ npt_cell_update_out( cells, cell_velocities, dt, cells_out=cells, device=device, _skip_validation=True, )
[docs] def npt_cell_update_out( cells: wp.array, cell_velocities: wp.array, dt: wp.array, cells_out: wp.array, device: str = None, _skip_validation: bool = False, ) -> wp.array: r"""Update cell matrices, writing results into a pre-allocated output. Non-mutating variant of :func:`npt_cell_update`, applying the multiplicative cell update .. math:: \mathbf{h}(t + \Delta t) = \mathbf{h} + \Delta t\,\dot{\varepsilon}\,\mathbf{h} into ``cells_out``. Parameters ---------- cells : wp.array(dtype=wp.mat33f or wp.mat33d) Input cell matrices. Shape (B,). Not modified when ``cells_out`` differs. cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). cells_out : wp.array(dtype=wp.mat33f or wp.mat33d) Pre-allocated output array for updated cell matrices. Shape (B,). device : str, optional Warp device. Default: inferred from ``cells``. Returns ------- wp.array(dtype=wp.mat33f or wp.mat33d) Updated cell matrices (same object as ``cells_out``). Shape (B,). See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.npt_cell_update` : In-place variant. """ if device is None: device = cells.device if not _skip_validation: validate_out_array(cells_out, cells, "cells_out") num_systems = cells.shape[0] wp.launch( _cell_update_kernel, dim=num_systems, inputs=[cells, cell_velocities, dt, cells_out], device=device, ) return cells_out
[docs] def run_npt_step( positions: wp.array, velocities: wp.array, forces: wp.array, masses: wp.array, cells: wp.array, cell_velocities: wp.array, virial_tensors: wp.array, eta: wp.array, eta_dot: wp.array, thermostat_masses: wp.array, cell_masses: wp.array, target_temperature: wp.array, target_pressure: wp.array, num_atoms: wp.array, chain_length: int, dt: wp.array, pressure_tensors: wp.array, volumes: wp.array, kinetic_energy: wp.array, # NOTE: ``cells_inv`` is at its pre-deprecation positional slot; # ``kinetic_tensors`` and ``num_atoms_per_system`` default to ``None`` # only so old positional callers (which passed ``cells_inv`` here) keep # working. Once ``cells_inv`` is removed the latter two can become # required positional arguments again. cells_inv: wp.array = None, kinetic_tensors: wp.array = None, num_atoms_per_system: wp.array = None, compute_forces_fn=None, batch_idx: wp.array = None, device: str = None, ) -> None: """ Perform a complete NPT integration step. Integration order: 1. Thermostat half-step 2. Barostat half-step 3. Velocity half-step 4. Position update 5. Cell update 6. Recompute forces 7. Velocity half-step 8. Barostat half-step 9. Thermostat half-step Particle thermostat friction is applied inside the two :func:`npt_velocity_half_step` calls via ``eta_dot[:, 0]``. The :func:`npt_thermostat_half_step` calls update the chain state; callers using a different split with explicit particle velocity scaling should not also use ``npt_velocity_half_step`` for those particle half-steps. Parameters ---------- positions, velocities, forces : wp.array Particle state arrays. MODIFIED in-place. masses : wp.array Particle masses. cells : wp.array Cell matrices. MODIFIED in-place. cell_velocities : wp.array Cell velocity matrices. MODIFIED in-place. virial_tensors : wp.array(dtype=vec9f or vec9d) Virial tensor. Updated by compute_forces_fn. eta, eta_dot : wp.array2d Thermostat state. MODIFIED in-place. thermostat_masses : wp.array2d Thermostat masses. cell_masses : wp.array Barostat masses. target_temperature, target_pressure : wp.array Target conditions. num_atoms : wp.array(dtype=wp.int32) Atom count for single-system mode. Shape (1,). chain_length : int Number of thermostats in chain. dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). pressure_tensors : wp.array(dtype=vec9f or vec9d) Scratch array for pressure tensor. Shape (B,). volumes : wp.array(dtype=scalar) Scratch array for cell volumes. Shape (B,). kinetic_energy : wp.array(dtype=scalar) Scratch array for kinetic energy. Shape (B,). Zeroed internally before each use. cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. kinetic_tensors : wp.array(dtype=scalar, ndim=2) Scratch array for kinetic tensor accumulation. Shape (B, 9). Zeroed internally before each use. num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). compute_forces_fn : callable, optional Force computation function. batch_idx : wp.array, optional System index for each atom. device : str, optional Warp device. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if kinetic_tensors is None or num_atoms_per_system is None: raise ValueError( "run_npt_step requires 'kinetic_tensors' and 'num_atoms_per_system'." ) if device is None: device = positions.device compute_pressure_tensor( velocities, masses, virial_tensors, cells, kinetic_tensors, pressure_tensors, volumes, batch_idx=batch_idx, device=device, ) # 1. Thermostat half-step npt_thermostat_half_step( eta, eta_dot, kinetic_energy, target_temperature, thermostat_masses, num_atoms_per_system, chain_length, dt, device=device, ) # 2. Barostat half-step npt_barostat_half_step( cell_velocities, pressure_tensors, target_pressure, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, device=device, ) # 3. Velocity half-step. Refresh caller-provided cells_inv for symmetry. if cells_inv is not None: compute_cell_inverse(cells, cells_inv=cells_inv, device=device) # Pass deprecated args (volumes / cells_inv) by keyword and omit them so # the orchestrator does not surface DeprecationWarnings the user never asked for. npt_velocity_half_step( velocities, masses, forces, cell_velocities, eta_dots=eta_dot, num_atoms=num_atoms, dt=dt, batch_idx=batch_idx, num_atoms_per_system=num_atoms_per_system, device=device, ) # 4. Position update npt_position_update( positions, velocities, cells, cell_velocities, dt, batch_idx=batch_idx, device=device, ) # 5. Cell update npt_cell_update(cells, cell_velocities, dt, device=device) # 6. Recompute forces if compute_forces_fn is not None: compute_forces_fn(positions, cells, forces, virial_tensors) # Recompute pressure, volumes, and cell inverses after cell update compute_cell_volume(cells, volumes=volumes, device=device) if cells_inv is not None: compute_cell_inverse(cells, cells_inv=cells_inv, device=device) compute_kinetic_energy( velocities, masses, kinetic_energy=kinetic_energy, batch_idx=batch_idx, device=device, ) compute_pressure_tensor( velocities, masses, virial_tensors, cells, kinetic_tensors, pressure_tensors, volumes, batch_idx=batch_idx, device=device, ) # 7. Velocity half-step npt_velocity_half_step( velocities, masses, forces, cell_velocities, eta_dots=eta_dot, num_atoms=num_atoms, dt=dt, batch_idx=batch_idx, num_atoms_per_system=num_atoms_per_system, device=device, ) # 8. Barostat half-step npt_barostat_half_step( cell_velocities, pressure_tensors, target_pressure, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, device=device, ) # 9. Thermostat half-step npt_thermostat_half_step( eta, eta_dot, kinetic_energy, target_temperature, thermostat_masses, num_atoms_per_system, chain_length, dt, device=device, )
# ============================================================================== # Functional Interfaces - NPH Integration # ==============================================================================
[docs] def nph_barostat_half_step( cell_velocities: wp.array, pressure_tensors: wp.array, target_pressures: wp.array, volumes: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, num_atoms_per_system: wp.array, dt: wp.array, device: str = None, ) -> None: r""" Perform barostat half-step for NPH ensemble (no thermostat coupling). NPH (isenthalpic-isobaric) simulations maintain constant pressure and enthalpy. Unlike NPT, there is no thermostat - the temperature evolves naturally on the constant-enthalpy surface. This function updates the cell velocity matrix :math:`\dot{h}` based on the pressure difference. The pressure control mode is automatically detected from the ``target_pressures`` array dtype: - **Isotropic** (scalar dtype): Uniform scaling in all directions - **Anisotropic/Orthorhombic** (vec3 dtype): Independent x, y, z control - **Triclinic** (vec9 dtype): Full stress tensor control Mathematical Formulation ------------------------ The cell velocity follows the MTK equations without thermostat: **Isotropic mode:** .. math:: \ddot{h} = \frac{V}{W}(P - P_{\text{ext}}) **Anisotropic mode:** .. math:: \ddot{h}_{ii} = \frac{V}{W}(P_{ii} - P_{\text{ext},ii}) **Triclinic mode:** .. math:: \ddot{h}_{ij} = \frac{V}{W}(P_{ij} - P_{\text{ext},ij}) Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). **MODIFIED in-place.** pressure_tensors : wp.array(dtype=vec9f or vec9d) Current pressure tensors from virial. Shape (B,). target_pressures : wp.array External/target pressure(s). The dtype determines the mode: - ``wp.float32`` or ``wp.float64``: Isotropic. Shape (B,). - ``wp.vec3f`` or ``wp.vec3d``: Anisotropic [Pxx, Pyy, Pzz]. Shape (B,). - ``vec9f`` or ``vec9d``: Full stress tensor. Shape (B,). volumes : wp.array(dtype=scalar) Cell volumes V. Shape (B,). cell_masses : wp.array(dtype=scalar) Barostat masses W. Shape (B,). kinetic_energy : wp.array(dtype=scalar) System kinetic energies. Shape (B,). num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). The half-step factor is applied internally. device : str, optional Warp device. Examples -------- Isotropic NPH: >>> target_P = wp.array([1.0], dtype=wp.float32, device="cuda:0") >>> nph_barostat_half_step( ... cell_velocities, pressure_tensors, target_P, ... volumes, cell_masses, kinetic_energy, ... num_atoms_per_system, dt=0.001 ... ) Anisotropic NPH: >>> target_P = wp.array([[1.0, 2.0, 1.5]], dtype=wp.vec3f, device="cuda:0") >>> nph_barostat_half_step( ... cell_velocities, pressure_tensors, target_P, ... ... ) See Also -------- npt_barostat_half_step : Barostat with thermostat coupling. run_nph_step : Complete NPH integration step. """ if device is None: device = cell_velocities.device num_systems = cell_velocities.shape[0] # Dispatch based on target_pressures dtype tp_dtype = target_pressures.dtype kernel = _NPH_BAROSTAT_KERNELS.get(tp_dtype) if kernel is None: raise ValueError( f"Unsupported target_pressures dtype: {tp_dtype}. " f"Expected scalar for isotropic, vec3 for anisotropic, vec9 for triclinic." ) wp.launch( kernel, dim=num_systems, inputs=[ cell_velocities, pressure_tensors, target_pressures, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, ], device=device, )
# Keep separate functions for backwards compatibility def nph_barostat_half_step_aniso( cell_velocities: wp.array, pressure_tensors: wp.array, target_pressures: wp.array, volumes: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, num_atoms_per_system: wp.array, dt: wp.array, device: str = None, ) -> None: r"""Anisotropic NPH barostat half-step with independent x, y, z pressure control. Updates the diagonal strain-rate components without thermostat coupling .. math:: \dot{\varepsilon}_{ii} \mathrel{+}= \frac{\Delta t}{2}\,\frac{V}{W} \left(P_{ii} + \frac{2\,\mathrm{KE}}{3 N V} - P_{\text{ext},ii}\right) Convenience alias for :func:`nph_barostat_half_step` with ``vec3`` target pressures. Prefer calling :func:`nph_barostat_half_step` directly; this function exists for backwards compatibility. Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). Modified in-place. pressure_tensors : wp.array(dtype=vec9f or vec9d) Current pressure tensors from virial. Shape (B,). target_pressures : wp.array(dtype=wp.vec3f or wp.vec3d) Target pressures [Pxx, Pyy, Pzz] per system. Shape (B,). volumes : wp.array(dtype=scalar) Cell volumes V. Shape (B,). cell_masses : wp.array(dtype=scalar) Barostat masses W. Shape (B,). kinetic_energy : wp.array(dtype=scalar) System kinetic energies. Shape (B,). num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system; half-step factor applied internally. Shape (B,). device : str, optional Warp device. Default: inferred from ``cell_velocities``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.nph_barostat_half_step` : Unified dispatcher for all modes. """ if device is None: device = cell_velocities.device num_systems = cell_velocities.shape[0] wp.launch( _nph_cell_velocity_update_aniso_kernel, dim=num_systems, inputs=[ cell_velocities, pressure_tensors, target_pressures, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, ], device=device, ) def nph_barostat_half_step_triclinic( cell_velocities: wp.array, pressure_tensors: wp.array, target_pressures: wp.array, volumes: wp.array, cell_masses: wp.array, kinetic_energy: wp.array, num_atoms_per_system: wp.array, dt: wp.array, device: str = None, ) -> None: r"""Triclinic NPH barostat half-step with full stress tensor control. Updates all nine strain-rate components without thermostat coupling .. math:: \dot{\varepsilon}_{ij} \mathrel{+}= \frac{\Delta t}{2}\,\frac{V}{W} \left(P_{ij} - P_{\text{ext},ij}\right) with the kinetic degrees-of-freedom correction :math:`+\,\tfrac{2 \mathrm{KE}}{3 N V}` added to the diagonal terms only. Convenience alias for :func:`nph_barostat_half_step` with ``vec9`` target pressures. Prefer calling :func:`nph_barostat_half_step` directly; this function exists for backwards compatibility. Parameters ---------- cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). Modified in-place. pressure_tensors : wp.array(dtype=vec9f or vec9d) Current pressure tensors from virial. Shape (B,). target_pressures : wp.array(dtype=vec9f or vec9d) Full target stress tensors [xx, xy, xz, yx, yy, yz, zx, zy, zz] per system. Shape (B,). volumes : wp.array(dtype=scalar) Cell volumes V. Shape (B,). cell_masses : wp.array(dtype=scalar) Barostat masses W. Shape (B,). kinetic_energy : wp.array(dtype=scalar) System kinetic energies. Shape (B,). num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system; half-step factor applied internally. Shape (B,). device : str, optional Warp device. Default: inferred from ``cell_velocities``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.nph_barostat_half_step` : Unified dispatcher for all modes. """ if device is None: device = cell_velocities.device num_systems = cell_velocities.shape[0] wp.launch( _nph_cell_velocity_update_triclinic_kernel, dim=num_systems, inputs=[ cell_velocities, pressure_tensors, target_pressures, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, ], device=device, )
[docs] def nph_velocity_half_step( velocities: wp.array, masses: wp.array, forces: wp.array, cell_velocities: wp.array, # NOTE: ``volumes``, ``num_atoms``, and ``dt`` default to ``None`` only # so legacy callers that still pass ``volumes`` positionally keep working # while the deprecation is staged. Once ``volumes`` is removed (next # minor cycle) these can become required positional arguments again. volumes: wp.array = None, num_atoms: wp.array = None, dt: wp.array = None, batch_idx: wp.array = None, num_atoms_per_system: wp.array = None, cells_inv: wp.array = None, mode: str = "isotropic", device: str = None, ) -> None: r""" Perform half-step velocity update for NPH ensemble (no thermostat). Updates particle velocities accounting for: 1. Forces from the potential energy surface 2. Coupling to barostat (cell velocity / strain rate) Unlike NPT, there is no thermostat coupling - the temperature evolves naturally on the constant-enthalpy surface. Mathematical Formulation ------------------------ **Isotropic mode** :math:`\mathbf{v} \leftarrow \mathbf{v} \exp\!\left(-\Delta t (1 + 1/N_{\text{atoms}}) \frac{\operatorname{Tr}(\dot{\varepsilon})}{3}\right)` **Anisotropic / triclinic mode** :math:`\mathbf{v} \leftarrow \mathbf{v} \exp\!\left(-\Delta t \left(\dot{\varepsilon} + \frac{\operatorname{Tr}(\dot{\varepsilon})}{3 N_{\text{atoms}}} I\right)\right)` Parameters ---------- velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. **MODIFIED in-place.** masses : wp.array(dtype=scalar) Particle masses. forces : wp.array(dtype=wp.vec3f or wp.vec3d) Forces on particles. cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon} = p_g/W`. volumes : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. num_atoms : wp.array(dtype=wp.int32) Atom count for single-system mode. Shape (1,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). The half-step factor is applied internally. batch_idx : wp.array, optional System index for each atom. num_atoms_per_system : wp.array, optional Number of atoms per system. cells_inv : wp.array(dtype=wp.mat33f or wp.mat33d), optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. mode : str, optional Pressure control mode: - ``"isotropic"``: scalar coupling on :math:`\operatorname{Tr}(\dot{\varepsilon})/3` - ``"anisotropic"``: diagonal coupling on :math:`\dot{\varepsilon}` - ``"triclinic"``: full-tensor coupling on :math:`\dot{\varepsilon}` device : str, optional Warp device. See Also -------- nph_barostat_half_step : Cell velocity update. npt_velocity_half_step : Velocity update with thermostat. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if volumes is not None: _warn_volumes_deprecated() volumes = None if num_atoms is None or dt is None: raise ValueError("nph_velocity_half_step requires 'num_atoms' and 'dt'.") # In-place: delegate to _out with velocities as both input and output nph_velocity_half_step_out( velocities, masses, forces, cell_velocities, volumes, num_atoms, dt, velocities_out=velocities, batch_idx=batch_idx, num_atoms_per_system=num_atoms_per_system, cells_inv=cells_inv, mode=mode, device=device, _skip_validation=True, )
# NOTE: nph_velocity_half_step_aniso removed - use nph_velocity_half_step(mode="anisotropic")
[docs] def nph_velocity_half_step_out( velocities: wp.array, masses: wp.array, forces: wp.array, cell_velocities: wp.array, # NOTE: ``volumes``, ``num_atoms``, ``dt``, and ``velocities_out`` default # to ``None`` only to keep legacy positional callers working while # ``volumes`` is being deprecated. Once ``volumes`` is removed they can # become required positional arguments again. volumes: wp.array = None, num_atoms: wp.array = None, dt: wp.array = None, velocities_out: wp.array = None, batch_idx: wp.array = None, num_atoms_per_system: wp.array = None, cells_inv: wp.array = None, mode: str = "isotropic", device: str = None, _skip_validation: bool = False, ) -> wp.array: r""" Perform half-step velocity update for NPH (non-mutating). Applies the same update as :func:`nph_velocity_half_step`, .. math:: \mathbf{v}(t + \tfrac{\Delta t}{2}) = \mathbf{v} + \frac{\Delta t}{2}\left(\frac{\mathbf{F}}{m} - \mathbf{D}(\dot{\varepsilon})\,\mathbf{v}\right) where :math:`\mathbf{D}` is the mode-dependent barostat drag (no thermostat term), but writes into ``velocities_out`` instead of mutating ``velocities``. Parameters ---------- velocities : wp.array Input velocities (not modified when velocities_out differs). masses, forces, cell_velocities : wp.array System state arrays. volumes : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. num_atoms : wp.array(dtype=wp.int32) Atom count for single-system mode. Shape (1,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). The half-step factor is applied internally. velocities_out : wp.array Pre-allocated output array. batch_idx, num_atoms_per_system : wp.array, optional For batched simulations. cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. mode : str, optional Pressure control mode: "isotropic", "anisotropic", or "triclinic". device : str, optional Warp device. Returns ------- wp.array Updated velocities. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if volumes is not None: _warn_volumes_deprecated() volumes = None if num_atoms is None or dt is None or velocities_out is None: raise ValueError( "nph_velocity_half_step_out requires 'num_atoms', 'dt', " "and 'velocities_out'." ) if device is None: device = velocities.device if not _skip_validation: validate_out_array(velocities_out, velocities, "velocities_out") h_inv = _ensure_cells_inv(cells_inv, volumes, cell_velocities, device) exec_mode = resolve_execution_mode(batch_idx, None) n_atoms = velocities.shape[0] if mode not in _NPH_VELOCITY_FAMILIES: raise ValueError( f"Unknown mode: '{mode}'. Expected 'isotropic', 'anisotropic', or 'triclinic'." ) family = _NPH_VELOCITY_FAMILIES[mode] launch_family( family, mode=exec_mode, dim=n_atoms, inputs_single=[ velocities, masses, forces, cell_velocities, h_inv, num_atoms, dt, velocities_out, ], inputs_batch=[ velocities, masses, forces, batch_idx, cell_velocities, h_inv, num_atoms_per_system, dt, velocities_out, ], device=device, ) return velocities_out
[docs] def nph_position_update( positions: wp.array, velocities: wp.array, cells: wp.array, cell_velocities: wp.array, dt: wp.array, cells_inv: wp.array = None, batch_idx: wp.array = None, device: str = None, ) -> None: r"""Update particle positions for NPH integration in-place. Applies the scaled-coordinate position update .. math:: \mathbf{r}(t + \Delta t) = \mathbf{r} + \Delta t\,(\mathbf{v} + \dot{\varepsilon}\,\mathbf{r}) Delegates to :func:`npt_position_update`; the position update kernel is identical for NPT and NPH. Parameters ---------- positions : wp.array(dtype=wp.vec3f or wp.vec3d) Particle positions. Shape (N,). Modified in-place. velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. Shape (N,). cells : wp.array(dtype=wp.mat33f or wp.mat33d) Cell matrices. Shape (B,). cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. batch_idx : wp.array(dtype=wp.int32), optional System index for each atom. Required for batched simulations. device : str, optional Warp device. Default: inferred from ``positions``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.nph_position_update_out` : Non-mutating variant. :func:`nvalchemiops.dynamics.integrators.npt.npt_position_update` : NPT equivalent. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None npt_position_update( positions, velocities, cells, cell_velocities, dt, cells_inv=cells_inv, batch_idx=batch_idx, device=device, )
[docs] def nph_position_update_out( positions: wp.array, velocities: wp.array, cells: wp.array, cell_velocities: wp.array, dt: wp.array, positions_out: wp.array, cells_inv: wp.array = None, batch_idx: wp.array = None, device: str = None, ) -> wp.array: r"""Update particle positions for NPH integration, writing results into a pre-allocated output. Non-mutating variant of :func:`nph_position_update`, applying the scaled-coordinate position update .. math:: \mathbf{r}(t + \Delta t) = \mathbf{r} + \Delta t\,(\mathbf{v} + \dot{\varepsilon}\,\mathbf{r}) Delegates to :func:`npt_position_update_out`; the position update kernel is identical for NPT and NPH. Parameters ---------- positions : wp.array(dtype=wp.vec3f or wp.vec3d) Input particle positions. Shape (N,). Not modified when ``positions_out`` differs. velocities : wp.array(dtype=wp.vec3f or wp.vec3d) Particle velocities. Shape (N,). cells : wp.array(dtype=wp.mat33f or wp.mat33d) Cell matrices. Shape (B,). cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). positions_out : wp.array(dtype=wp.vec3f or wp.vec3d) Pre-allocated output array for updated positions. Shape (N,). cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. batch_idx : wp.array(dtype=wp.int32), optional System index for each atom. Required for batched simulations. device : str, optional Warp device. Default: inferred from ``positions``. Returns ------- wp.array(dtype=wp.vec3f or wp.vec3d) Updated positions array (same object as ``positions_out``). Shape (N,). See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.nph_position_update` : In-place variant. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None return npt_position_update_out( positions, velocities, cells, cell_velocities, dt, positions_out, cells_inv=cells_inv, batch_idx=batch_idx, device=device, )
[docs] def nph_cell_update( cells: wp.array, cell_velocities: wp.array, dt: wp.array, device: str = None, ) -> None: r"""Update cell matrices for NPH integration in-place. Applies the multiplicative cell update .. math:: \mathbf{h}(t + \Delta t) = \mathbf{h} + \Delta t\,\dot{\varepsilon}\,\mathbf{h} Delegates to :func:`npt_cell_update`; the cell update kernel is identical for NPT and NPH. Parameters ---------- cells : wp.array(dtype=wp.mat33f or wp.mat33d) Cell matrices. Shape (B,). Modified in-place. cell_velocities : wp.array(dtype=wp.mat33f or wp.mat33d) Strain-rate matrices :math:`\dot{\varepsilon}`. Shape (B,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). device : str, optional Warp device. Default: inferred from ``cells``. See Also -------- :func:`nvalchemiops.dynamics.integrators.npt.npt_cell_update` : NPT equivalent. """ npt_cell_update(cells, cell_velocities, dt, device=device)
[docs] def run_nph_step( positions: wp.array, velocities: wp.array, forces: wp.array, masses: wp.array, cells: wp.array, cell_velocities: wp.array, virial_tensors: wp.array, cell_masses: wp.array, target_pressure: wp.array, num_atoms: wp.array, dt: wp.array, pressure_tensors: wp.array, volumes: wp.array, kinetic_energy: wp.array, # NOTE: ``cells_inv`` is at its pre-deprecation positional slot; # ``kinetic_tensors`` and ``num_atoms_per_system`` default to ``None`` # only so old positional callers (which passed ``cells_inv`` here) keep # working. Once ``cells_inv`` is removed the latter two can become # required positional arguments again. cells_inv: wp.array = None, kinetic_tensors: wp.array = None, num_atoms_per_system: wp.array = None, compute_forces_fn=None, batch_idx: wp.array = None, device: str = None, ) -> None: """ Perform a complete NPH integration step. Integration order (no thermostat): 1. Barostat half-step 2. Velocity half-step 3. Position update 4. Cell update 5. Recompute forces 6. Velocity half-step 7. Barostat half-step Parameters ---------- positions, velocities, forces : wp.array Particle state arrays. MODIFIED in-place. masses : wp.array Particle masses. cells : wp.array Cell matrices. MODIFIED in-place. cell_velocities : wp.array Cell velocity matrices. MODIFIED in-place. virial_tensors : wp.array(dtype=vec9f or vec9d) Virial tensor. Updated by compute_forces_fn. cell_masses : wp.array Barostat masses. target_pressure : wp.array Target/external pressures. num_atoms : wp.array(dtype=wp.int32) Atom count for single-system mode. Shape (1,). dt : wp.array(dtype=scalar) Full time step per system. Shape (B,). pressure_tensors : wp.array(dtype=vec9f or vec9d) Scratch array for pressure tensor. Shape (B,). volumes : wp.array(dtype=scalar) Scratch array for cell volumes. Shape (B,). kinetic_energy : wp.array(dtype=scalar) Scratch array for kinetic energy. Shape (B,). Zeroed internally before each use. cells_inv : wp.array, optional .. deprecated:: 0.3.1 Ignored; ``cell_velocities`` is the strain rate ``eps_dot = p_g/W``. kinetic_tensors : wp.array(dtype=scalar, ndim=2) Scratch array for kinetic tensor accumulation. Shape (B, 9). Zeroed internally before each use. num_atoms_per_system : wp.array(dtype=wp.int32) Number of atoms per system. Shape (B,). compute_forces_fn : callable, optional Force computation function. batch_idx : wp.array, optional System index for each atom. device : str, optional Warp device. """ if cells_inv is not None: _warn_cells_inv_deprecated() cells_inv = None if kinetic_tensors is None or num_atoms_per_system is None: raise ValueError( "run_nph_step requires 'kinetic_tensors' and 'num_atoms_per_system'." ) if device is None: device = positions.device compute_pressure_tensor( velocities, masses, virial_tensors, cells, kinetic_tensors, pressure_tensors, volumes, batch_idx=batch_idx, device=device, ) # 1. Barostat half-step nph_barostat_half_step( cell_velocities, pressure_tensors, target_pressure, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, device=device, ) # 2. Velocity half-step if cells_inv is not None: compute_cell_inverse(cells, cells_inv=cells_inv, device=device) # Pass deprecated args (volumes / cells_inv) by keyword and omit them so # the orchestrator does not surface DeprecationWarnings the user never asked for. nph_velocity_half_step( velocities, masses, forces, cell_velocities, num_atoms=num_atoms, dt=dt, batch_idx=batch_idx, num_atoms_per_system=num_atoms_per_system, device=device, ) # 3. Position update nph_position_update( positions, velocities, cells, cell_velocities, dt, batch_idx=batch_idx, device=device, ) # 4. Cell update nph_cell_update(cells, cell_velocities, dt, device=device) # 5. Recompute forces if compute_forces_fn is not None: compute_forces_fn(positions, cells, forces, virial_tensors) # Recompute pressure, volumes, and cell inverses after cell update compute_cell_volume(cells, volumes=volumes, device=device) if cells_inv is not None: compute_cell_inverse(cells, cells_inv=cells_inv, device=device) compute_kinetic_energy( velocities, masses, kinetic_energy=kinetic_energy, batch_idx=batch_idx, device=device, ) compute_pressure_tensor( velocities, masses, virial_tensors, cells, kinetic_tensors, pressure_tensors, volumes, batch_idx=batch_idx, device=device, ) # 6. Velocity half-step nph_velocity_half_step( velocities, masses, forces, cell_velocities, num_atoms=num_atoms, dt=dt, batch_idx=batch_idx, num_atoms_per_system=num_atoms_per_system, device=device, ) # 7. Barostat half-step nph_barostat_half_step( cell_velocities, pressure_tensors, target_pressure, volumes, cell_masses, kinetic_energy, num_atoms_per_system, dt, device=device, )