# 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"""
PyTorch Adapter for FIRE2 Optimizer
====================================
Thin wrapper that accepts PyTorch tensors, allocates scratch buffers via
PyTorch's CUDA caching allocator, and calls the pure-Warp FIRE2 kernels.
Every entry point runs the FIRE2 scheme of Guenole et al. (2020): the
leapfrog half-step is deferred into the reduction, so the per-system power is
measured on the post-kick velocity and the half-step is fused into the mixing:
.. math::
P = \sum_i (\mathbf{v}_i + \Delta t\,\mathbf{F}_i) \cdot \mathbf{F}_i,\qquad
\mathbf{v} = (1-\alpha)\,\mathbf{v}
+ \left[(1-\alpha)\,\Delta t
+ \alpha\sqrt{\tfrac{\mathbf{v}\cdot\mathbf{v}}
{\mathbf{F}\cdot\mathbf{F}}}\right]\mathbf{F}
The displacement is :math:`\Delta\mathbf{r} = \Delta t\,\mathbf{v}` downhill and
:math:`-\tfrac{1}{2}\Delta t\,\mathbf{v}` uphill (:math:`P \le 0`, velocity then
zeroed), clamped by :math:`\min(1, \text{maxstep}/\lVert\Delta\mathbf{r}\rVert)`
per system with :math:`\Delta t` scaled by the same factor.
Entry points:
- :func:`fire2_step_coord` -- coordinate-only optimization.
- :func:`fire2_step_coord_cell` -- variable-cell optimization
(coordinates + cell DOFs). Packs atomic and cell DOFs into an
interleaved extended velocity/force representation, runs FIRE2
mixing on the generalized DOFs, then applies a coupled atomic/cell
position update.
- :func:`fire2_step_extended` -- run FIRE2 directly on caller-managed
extended arrays (no per-step pack/unpack overhead).
Modifies inputs in-place. Scratch buffers and static metadata
(``atom_ptr``, ``ext_atom_ptr``, ``ext_batch_idx``) can be passed in
for reuse across steps, or left as ``None`` to allocate internally
each call. See :func:`fire2_step_coord_cell` docstring for
pre-computation recipes.
"""
from __future__ import annotations
import warnings
import torch
import warp as wp
from nvalchemiops.batch_utils import atom_ptr_to_batch_idx, batch_idx_to_atom_ptr
from nvalchemiops.dynamics.optimizers.fire2 import (
fire2_reduce,
fire2_step,
fire2_update,
)
from nvalchemiops.dynamics.utils.cell_filter import (
_apply_fire2_coord_cell_step,
_fire2_coord_cell_clamp_apply,
_fire2_coord_cell_compute_max_norm,
extend_atom_ptr,
pack_forces_with_cell,
pack_velocities_with_cell,
unpack_velocities_with_cell,
)
# Torch dtype -> Warp dtype mappings
_TORCH_TO_WP_VEC = {torch.float32: wp.vec3f, torch.float64: wp.vec3d}
_TORCH_TO_WP_MAT = {torch.float32: wp.mat33f, torch.float64: wp.mat33d}
def _alloc_or_zero(
buf: torch.Tensor | None, size: int, dtype: torch.dtype, device: torch.device
) -> torch.Tensor:
"""Return a zeroed buffer, allocating one if *buf* is ``None``."""
if buf is None:
return torch.zeros(size, dtype=dtype, device=device)
buf.zero_()
return buf
def _coord_cell_ext_metadata(
batch_idx: torch.Tensor,
M: int,
device: torch.device,
wp_device,
*,
atom_ptr: torch.Tensor | None,
ext_atom_ptr: torch.Tensor | None,
ext_batch_idx: torch.Tensor | None,
):
"""Return the packed-layout metadata for variable-cell FIRE2.
Computes ``atom_ptr`` / ``ext_atom_ptr`` / ``ext_batch_idx`` (as Warp arrays)
for the interleaved [atoms, 2 cell rows] layout, filling any not supplied.
"""
N = batch_idx.shape[0]
N_ext = N + 2 * M
wp_bidx = wp.from_torch(batch_idx.detach(), dtype=wp.int32)
if atom_ptr is None:
atom_ptr = torch.zeros(M + 1, dtype=torch.int32, device=device)
atom_counts = torch.zeros(M, dtype=torch.int32, device=device)
batch_idx_to_atom_ptr(
wp_bidx,
wp.from_torch(atom_counts, dtype=wp.int32),
wp.from_torch(atom_ptr, dtype=wp.int32),
)
wp_atom_ptr = wp.from_torch(atom_ptr, dtype=wp.int32)
if ext_atom_ptr is None:
ext_atom_ptr = torch.zeros(M + 1, dtype=torch.int32, device=device)
extend_atom_ptr(
wp_atom_ptr,
wp.from_torch(ext_atom_ptr, dtype=wp.int32),
device=wp_device,
)
wp_ext_atom_ptr = wp.from_torch(ext_atom_ptr, dtype=wp.int32)
if ext_batch_idx is None:
ext_batch_idx = torch.empty(N_ext, dtype=torch.int32, device=device)
atom_ptr_to_batch_idx(
wp_ext_atom_ptr,
wp.from_torch(ext_batch_idx, dtype=wp.int32),
)
wp_ext_batch_idx = wp.from_torch(ext_batch_idx, dtype=wp.int32)
return atom_ptr, wp_atom_ptr, wp_ext_atom_ptr, wp_ext_batch_idx
def _coord_cell_mix_impl(
positions,
velocities,
forces,
cell,
cell_velocities,
cell_force,
batch_idx,
alpha,
dt,
nsteps_inc,
*,
atom_ptr,
ext_atom_ptr,
ext_velocities,
ext_forces,
ext_batch_idx,
vf,
v_sumsq,
f_sumsq,
max_norm,
delaystep,
dtgrow,
dtshrink,
alphashrink,
alpha0,
tmax,
tmin,
cell_force_scale,
compute_reductions,
ext_positions,
):
"""Shared front half of the variable-cell FIRE2 step.
Packs atomic + cell DOFs, runs the FIRE2 reduction (gated by
``compute_reductions``) and velocity mix on the packed DOFs, and unpacks the
mixed velocities. Does **not** apply positions/cell. Returns the Warp
handles the apply phase needs, or ``None`` for an empty system.
"""
dtype = positions.dtype
device = positions.device
N = positions.shape[0]
M = alpha.shape[0]
N_ext = N + 2 * M
vec_type = _TORCH_TO_WP_VEC[dtype]
mat_type = _TORCH_TO_WP_MAT[dtype]
wp_device = wp.device_from_torch(device)
if cell_force_scale <= 0.0:
raise ValueError("cell_force_scale must be positive")
if ext_positions is not None:
warnings.warn(
"fire2_step_coord_cell(..., ext_positions=...) is deprecated and ignored.",
DeprecationWarning,
stacklevel=3,
)
if N == 0:
for buf in (vf, v_sumsq, f_sumsq, max_norm):
if buf is not None:
buf.zero_()
return None
if ext_velocities is None:
ext_velocities = torch.empty(N_ext, 3, dtype=dtype, device=device)
if ext_forces is None:
ext_forces = torch.empty(N_ext, 3, dtype=dtype, device=device)
if compute_reductions:
vf = _alloc_or_zero(vf, M, dtype, device)
v_sumsq = _alloc_or_zero(v_sumsq, M, dtype, device)
f_sumsq = _alloc_or_zero(f_sumsq, M, dtype, device)
elif vf is None or v_sumsq is None or f_sumsq is None:
raise ValueError(
"vf, v_sumsq, f_sumsq must be provided when compute_reductions=False"
)
max_norm = _alloc_or_zero(max_norm, M, dtype, device)
wp_pos = wp.from_torch(positions.detach(), dtype=vec_type)
wp_vel = wp.from_torch(velocities.detach(), dtype=vec_type)
wp_forces = wp.from_torch(forces.detach(), dtype=vec_type)
wp_cell = wp.from_torch(cell.detach(), dtype=mat_type)
wp_cell_vel = wp.from_torch(cell_velocities.detach(), dtype=mat_type)
wp_bidx = wp.from_torch(batch_idx.detach(), dtype=wp.int32)
wp_ext_vel = wp.from_torch(ext_velocities, dtype=vec_type)
wp_ext_forces = wp.from_torch(ext_forces, dtype=vec_type)
atom_ptr, wp_atom_ptr, wp_ext_atom_ptr, wp_ext_batch_idx = _coord_cell_ext_metadata(
batch_idx,
M,
device,
wp_device,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_batch_idx=ext_batch_idx,
)
atom_counts = atom_ptr[1:] - atom_ptr[:-1]
if torch.any(atom_counts <= 0).item():
raise ValueError("fire2_step_coord_cell requires at least one atom per system")
cell_force_divisor = atom_counts.to(dtype=dtype).reshape(M, 1, 1) * cell_force_scale
cell_force_work = (cell_force.detach() / cell_force_divisor).contiguous()
wp_cell_force = wp.from_torch(cell_force_work, dtype=mat_type)
if M == 1:
pack_velocities_with_cell(wp_vel, wp_cell_vel, wp_ext_vel, device=wp_device)
pack_forces_with_cell(wp_forces, wp_cell_force, wp_ext_forces, device=wp_device)
else:
pack_velocities_with_cell(
wp_vel,
wp_cell_vel,
wp_ext_vel,
wp_atom_ptr,
wp_ext_atom_ptr,
device=wp_device,
batch_idx=wp_bidx,
)
pack_forces_with_cell(
wp_forces,
wp_cell_force,
wp_ext_forces,
wp_atom_ptr,
wp_ext_atom_ptr,
device=wp_device,
batch_idx=wp_bidx,
)
wp_dt = wp.from_torch(dt.detach())
wp_vf = wp.from_torch(vf)
wp_max_norm = wp.from_torch(max_norm)
fire2_update(
wp_ext_vel,
wp_ext_forces,
wp_ext_batch_idx,
wp.from_torch(alpha.detach()),
wp_dt,
wp.from_torch(nsteps_inc.detach(), dtype=wp.int32),
wp_vf,
wp.from_torch(v_sumsq),
wp.from_torch(f_sumsq),
wp_max_norm,
delaystep=delaystep,
dtgrow=dtgrow,
dtshrink=dtshrink,
alphashrink=alphashrink,
alpha0=alpha0,
tmax=tmax,
tmin=tmin,
compute_max_norm=False,
compute_reductions=compute_reductions,
)
if M == 1:
unpack_velocities_with_cell(
wp_ext_vel, wp_vel, wp_cell_vel, num_atoms=N, device=wp_device
)
else:
unpack_velocities_with_cell(
wp_ext_vel,
wp_vel,
wp_cell_vel,
atom_ptr=wp_atom_ptr,
ext_atom_ptr=wp_ext_atom_ptr,
device=wp_device,
batch_idx=wp_bidx,
)
return (
wp_pos,
wp_vel,
wp_cell,
wp_cell_vel,
wp_dt,
wp_vf,
wp_ext_batch_idx,
wp_ext_atom_ptr,
wp_max_norm,
wp_device,
)
[docs]
def fire2_step_coord(
positions: torch.Tensor,
velocities: torch.Tensor,
forces: torch.Tensor,
batch_idx: torch.Tensor,
alpha: torch.Tensor,
dt: torch.Tensor,
nsteps_inc: torch.Tensor,
*,
vf: torch.Tensor | None = None,
v_sumsq: torch.Tensor | None = None,
f_sumsq: torch.Tensor | None = None,
max_norm: torch.Tensor | None = None,
delaystep: int = 60,
dtgrow: float = 1.05,
dtshrink: float = 0.75,
alphashrink: float = 0.985,
alpha0: float = 0.09,
tmax: float = 0.08,
tmin: float = 0.005,
maxstep: float = 0.1,
compute_reductions: bool = True,
) -> None:
r"""FIRE2 coordinate-only optimization step.
Converts PyTorch tensors to Warp arrays (zero-copy) and delegates to
the pure-Warp :func:`~nvalchemiops.dynamics.optimizers.fire2.fire2_step`.
The deferred half-step is folded into the reduction, so the per-system
power is measured on the post-kick velocity, and the mixing fuses that
half-step with the FIRE2 rule:
.. math::
P = \sum_i (\mathbf{v}_i + \Delta t\,\mathbf{F}_i) \cdot \mathbf{F}_i,\qquad
\mathbf{v} = (1-\alpha)\,\mathbf{v}
+ \left[(1-\alpha)\,\Delta t
+ \alpha\sqrt{\tfrac{\mathbf{v}\cdot\mathbf{v}}
{\mathbf{F}\cdot\mathbf{F}}}\right]\mathbf{F}
The displacement is :math:`\Delta\mathbf{r} = \Delta t\,\mathbf{v}` downhill
and :math:`-\tfrac{1}{2}\Delta t\,\mathbf{v}` uphill (:math:`P \le 0`,
velocity then zeroed), capped by
:math:`\min(1, \text{maxstep}/\lVert\Delta\mathbf{r}\rVert)` per system with
:math:`\Delta t` scaled by the same factor.
Modifies *positions*, *velocities*, *alpha*, *dt*, and *nsteps_inc*
in-place.
Parameters
----------
positions : Tensor, shape (N, 3), dtype float32/float64
Atomic positions.
velocities : Tensor, shape (N, 3), dtype float32/float64
Atomic velocities.
forces : Tensor, shape (N, 3), dtype float32/float64
Forces on atoms (read-only).
batch_idx : Tensor, shape (N,), dtype int32
Sorted system index per atom. Must be non-decreasing;
segmented reductions rely on contiguous atom ranges.
alpha : Tensor, shape (M,), dtype float32/float64
FIRE2 mixing parameter (one per system).
dt : Tensor, shape (M,), dtype float32/float64
Per-system timestep.
nsteps_inc : Tensor, shape (M,), dtype int32
Consecutive positive-power step counter.
vf, v_sumsq, f_sumsq, max_norm : Tensor, shape (M,), optional
Scratch buffers for per-system reductions. Allocated and zeroed
if ``None``; zeroed in-place if provided. Pre-allocate and pass
them in tight loops to avoid repeated allocation::
M = alpha.shape[0]
vf = torch.empty(M, dtype=positions.dtype,
device=positions.device)
v_sumsq = torch.empty_like(vf)
f_sumsq = torch.empty_like(vf)
max_norm = torch.empty_like(vf)
delaystep, dtgrow, dtshrink, alphashrink, alpha0, tmax, tmin, maxstep
FIRE2 hyperparameters. See
:func:`~nvalchemiops.dynamics.optimizers.fire2.fire2_step` for
defaults and descriptions.
compute_reductions : bool, default True
If True, recompute ``vf``/``v_sumsq``/``f_sumsq`` internally. If False,
use the caller-supplied values in those buffers instead of recomputing
them (they must be provided and are not zeroed); the ``maxstep`` clamp
still uses this call's internally-computed ``max_norm``.
Notes
-----
Default hyperparameters (from the underlying Warp implementation):
``delaystep=60``, ``dtgrow=1.05``, ``dtshrink=0.75``,
``alphashrink=0.985``, ``alpha0=0.09``, ``tmax=0.08``,
``tmin=0.005``, ``maxstep=0.1``.
See :func:`nvalchemiops.dynamics.optimizers.fire2.fire2_step` for
full descriptions.
For variable-cell optimization (coordinates + cell DOFs), use
:func:`fire2_step_coord_cell` instead.
Examples
--------
Minimal single-step call:
>>> fire2_step_coord(
... positions, velocities, forces,
... batch_idx, alpha, dt, nsteps_inc,
... )
Tight optimization loop with pre-allocated scratch buffers:
>>> M = alpha.shape[0]
>>> vf = torch.empty(M, dtype=positions.dtype, device=positions.device)
>>> v_sumsq = torch.empty_like(vf)
>>> f_sumsq = torch.empty_like(vf)
>>> max_norm = torch.empty_like(vf)
>>> for step in range(num_steps):
... fire2_step_coord(
... positions, velocities, forces,
... batch_idx, alpha, dt, nsteps_inc,
... vf=vf, v_sumsq=v_sumsq,
... f_sumsq=f_sumsq, max_norm=max_norm,
... )
"""
dtype = positions.dtype
device = positions.device
M = alpha.shape[0]
vec_type = _TORCH_TO_WP_VEC[dtype]
# Scratch buffers: allocate/zero when recomputing; require and preserve
# them when the caller supplies precomputed reductions.
if compute_reductions:
vf = _alloc_or_zero(vf, M, dtype, device)
v_sumsq = _alloc_or_zero(v_sumsq, M, dtype, device)
f_sumsq = _alloc_or_zero(f_sumsq, M, dtype, device)
elif vf is None or v_sumsq is None or f_sumsq is None:
raise ValueError(
"vf, v_sumsq, f_sumsq must be provided when compute_reductions=False"
)
max_norm = _alloc_or_zero(max_norm, M, dtype, device)
if positions.shape[0] == 0:
return
# Delegate to the Warp-level fire2_step
fire2_step(
wp.from_torch(positions.detach(), dtype=vec_type),
wp.from_torch(velocities.detach(), dtype=vec_type),
wp.from_torch(forces.detach(), dtype=vec_type),
wp.from_torch(batch_idx.detach(), dtype=wp.int32),
wp.from_torch(alpha.detach()),
wp.from_torch(dt.detach()),
wp.from_torch(nsteps_inc.detach(), dtype=wp.int32),
wp.from_torch(vf),
wp.from_torch(v_sumsq),
wp.from_torch(f_sumsq),
wp.from_torch(max_norm),
delaystep=delaystep,
dtgrow=dtgrow,
dtshrink=dtshrink,
alphashrink=alphashrink,
alpha0=alpha0,
tmax=tmax,
tmin=tmin,
maxstep=maxstep,
compute_reductions=compute_reductions,
)
[docs]
def fire2_step_coord_cell(
positions: torch.Tensor,
velocities: torch.Tensor,
forces: torch.Tensor,
cell: torch.Tensor,
cell_velocities: torch.Tensor,
cell_force: torch.Tensor,
batch_idx: torch.Tensor,
alpha: torch.Tensor,
dt: torch.Tensor,
nsteps_inc: torch.Tensor,
*,
atom_ptr: torch.Tensor | None = None,
ext_atom_ptr: torch.Tensor | None = None,
ext_positions: torch.Tensor | None = None,
ext_velocities: torch.Tensor | None = None,
ext_forces: torch.Tensor | None = None,
ext_batch_idx: torch.Tensor | None = None,
vf: torch.Tensor | None = None,
v_sumsq: torch.Tensor | None = None,
f_sumsq: torch.Tensor | None = None,
max_norm: torch.Tensor | None = None,
delaystep: int = 60,
dtgrow: float = 1.05,
dtshrink: float = 0.75,
alphashrink: float = 0.985,
alpha0: float = 0.09,
tmax: float = 0.08,
tmin: float = 0.005,
maxstep: float = 0.1,
cell_force_scale: float = 1.0,
compute_reductions: bool = True,
) -> None:
r"""FIRE2 variable-cell optimization step.
Performs a FIRE2 step on both atomic coordinates and cell degrees of
freedom. Internally packs atomic + cell velocity/force DOFs into an
**interleaved layout** (each system's atoms followed by its 2 cell
vec3s), runs the FIRE2 reduction + mixing phase on those generalized
DOFs, unpacks the mixed velocities, and then applies the physically
coupled atomic/cell update directly on the caller's coordinate and cell
tensors.
FIRE2 runs on the generalized DOF vectors :math:`\mathbf{v}`,
:math:`\mathbf{F}` that stack the atomic velocities/forces and the (scaled)
cell velocity/force rows. The power and mixing are taken over those
generalized DOFs:
.. math::
P = \sum_i (\mathbf{v}_i + \Delta t\,\mathbf{F}_i) \cdot \mathbf{F}_i,\qquad
\mathbf{v} = (1-\alpha)\,\mathbf{v}
+ \left[(1-\alpha)\,\Delta t
+ \alpha\sqrt{\tfrac{\mathbf{v}\cdot\mathbf{v}}
{\mathbf{F}\cdot\mathbf{F}}}\right]\mathbf{F}
where :math:`\mathbf{v}\cdot\mathbf{v}` and :math:`\mathbf{F}\cdot\mathbf{F}`
sum over each system's atomic and cell DOFs. The mixed generalized velocity
is then applied as a coupled atomic/cell position update: cell DOFs deform
the cell while atoms follow the affine cell remap plus their own coordinate
motion. The resulting Cartesian atomic displacement is capped by
:math:`\min(1, \text{maxstep}/\lVert\Delta\mathbf{r}\rVert)` per system
(uphill systems, :math:`P \le 0`, take the :math:`-\tfrac{1}{2}\Delta t`
correction and zero their velocities), with :math:`\Delta t` scaled by the
same factor.
The cell must be pre-aligned to upper-triangular form via
:func:`nvalchemiops.dynamics.utils.cell_filter.align_cell` before
the first call.
Modifies *positions*, *velocities*, *cell*, *cell_velocities*,
*alpha*, *dt*, and *nsteps_inc* in-place.
Parameters
----------
positions : Tensor, shape (N, 3), dtype float32/float64
Atomic positions.
velocities : Tensor, shape (N, 3), dtype float32/float64
Atomic velocities.
forces : Tensor, shape (N, 3), dtype float32/float64
Forces on atoms (read-only).
cell : Tensor, shape (M, 3, 3), dtype float32/float64
Cell matrices (upper-triangular from ``align_cell()``).
cell_velocities : Tensor, shape (M, 3, 3), dtype float32/float64
Cell velocity matrices.
cell_force : Tensor, shape (M, 3, 3), dtype float32/float64
Raw cell force matrices from ``stress_to_cell_force()`` (read-only).
These are divided by ``atoms_per_system * cell_force_scale`` before
FIRE2 velocity mixing.
batch_idx : Tensor, shape (N,), dtype int32
Sorted system index per atom.
alpha : Tensor, shape (M,), dtype float32/float64
FIRE2 mixing parameter.
dt : Tensor, shape (M,), dtype float32/float64
Per-system timestep.
nsteps_inc : Tensor, shape (M,), dtype int32
Consecutive positive-power counter.
atom_ptr : Tensor, shape (M+1,), dtype int32, optional
CSR-style atom pointers derived from *batch_idx*. If ``None``,
computed internally each call via
:func:`~nvalchemiops.batch_utils.batch_idx_to_atom_ptr`.
Pre-compute once and pass in tight loops to avoid repeated
allocation. See *Notes* for how to compute.
ext_atom_ptr : Tensor, shape (M+1,), dtype int32, optional
Extended atom pointers (accounts for 2 cell DOFs per system).
If ``None``, computed from *atom_ptr* each call via
:func:`~nvalchemiops.dynamics.utils.cell_filter.extend_atom_ptr`.
See *Notes* for how to compute.
ext_positions : Tensor, shape (N+2M, 3), optional
Deprecated and ignored.
``ext_positions`` is accepted only for backward compatibility.
The coupled variable-cell FIRE2 path no longer updates packed
positions directly.
ext_velocities, ext_forces : Tensor, shape (N+2M, 3), optional
Pre-allocated extended working arrays for the FIRE2 generalized-DOF
reduction and mixing phase. Allocated if ``None``; contents are
overwritten each call.
ext_batch_idx : Tensor, shape (N+2M,), dtype int32, optional
Pre-computed extended batch index (sorted, matching interleaved
pack layout). If ``None``, computed from *ext_atom_ptr* each
call via
:func:`~nvalchemiops.batch_utils.atom_ptr_to_batch_idx`.
If provided, assumed correct and reused without recomputation.
See *Notes* for how to compute.
vf, v_sumsq, f_sumsq, max_norm : Tensor, shape (M,), optional
Scratch buffers for reductions. Allocated and zeroed if ``None``;
zeroed in-place if provided. In this coupled cell adapter,
``max_norm`` is the final physical Cartesian atomic displacement norm,
recomputed after cell motion is coupled back to the atoms.
delaystep, dtgrow, dtshrink, alphashrink, alpha0, tmax, tmin, maxstep
FIRE2 hyperparameters.
cell_force_scale : float, default=1.0
Extra positive multiplier for stress-derived cell-force normalization.
Cell forces are divided by ``atoms_per_system * cell_force_scale``.
compute_reductions : bool, default True
If True, recompute ``vf``/``v_sumsq``/``f_sumsq`` internally. If False,
use the caller-supplied values instead (they must be provided and are
not zeroed). These reductions are over the **generalized (atom + cell)
DOFs** of each system, so a caller assembling them across a partition
must include the (replicated) cell contribution exactly once. The
``maxstep`` clamp still uses this call's internally-recomputed
``max_norm`` (the physical Cartesian displacement after cell coupling).
Notes
-----
Default hyperparameters (from the underlying Warp implementation):
``delaystep=60``, ``dtgrow=1.05``, ``dtshrink=0.75``,
``alphashrink=0.985``, ``alpha0=0.09``, ``tmax=0.08``,
``tmin=0.005``, ``maxstep=0.1``.
See :func:`nvalchemiops.dynamics.optimizers.fire2.fire2_step` for
full descriptions.
The high-level variable-cell adapter normalizes raw stress-derived cell
forces by the number of atoms in each system. ``cell_force_scale`` is an
extra multiplier on top of that per-system atom-count normalization.
**Pre-computing static metadata for tight loops**
When *batch_idx* does not change between steps (fixed system sizes),
*atom_ptr*, *ext_atom_ptr*, and *ext_batch_idx* are constant and can
be pre-computed once to eliminate per-step allocation and kernel
launches::
import warp as wp
from nvalchemiops.batch_utils import (
atom_ptr_to_batch_idx,
batch_idx_to_atom_ptr,
)
from nvalchemiops.dynamics.utils.cell_filter import extend_atom_ptr
N, M = positions.shape[0], alpha.shape[0]
N_ext = N + 2 * M
device = positions.device
# 1) atom_ptr from batch_idx (CSR pointers into atom array)
atom_ptr = torch.zeros(M + 1, dtype=torch.int32, device=device)
atom_counts = torch.zeros(M, dtype=torch.int32, device=device)
batch_idx_to_atom_ptr(
wp.from_torch(batch_idx, dtype=wp.int32),
wp.from_torch(atom_counts, dtype=wp.int32),
wp.from_torch(atom_ptr, dtype=wp.int32),
)
# 2) ext_atom_ptr (CSR pointers into extended array,
# each system's range grows by 2 for the cell DOFs)
ext_atom_ptr = torch.zeros(M + 1, dtype=torch.int32, device=device)
extend_atom_ptr(
wp.from_torch(atom_ptr, dtype=wp.int32),
wp.from_torch(ext_atom_ptr, dtype=wp.int32),
)
# 3) ext_batch_idx (sorted system index for extended array)
ext_batch_idx = torch.empty(N_ext, dtype=torch.int32, device=device)
atom_ptr_to_batch_idx(
wp.from_torch(ext_atom_ptr, dtype=wp.int32),
wp.from_torch(ext_batch_idx, dtype=wp.int32),
)
Then pass all three on every step::
fire2_step_coord_cell(
...,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_batch_idx=ext_batch_idx,
)
**Extended array layout (interleaved)**
The mixing phase places each system's cell DOFs immediately after its
atoms::
[sys0_atom0, ..., sys0_atomK, sys0_cell_row0, sys0_cell_row1,
sys1_atom0, ..., sys1_atomJ, sys1_cell_row0, sys1_cell_row1, ...]
This ensures that *ext_batch_idx* is sorted (all DOFs for system 0
precede all DOFs for system 1, etc.), which is required by
``fire2_step``'s segmented reductions.
Examples
--------
Minimal single-step call (all buffers allocated internally):
>>> fire2_step_coord_cell(
... positions, velocities, forces,
... cell, cell_velocities, cell_force,
... batch_idx, alpha, dt, nsteps_inc,
... )
Tight optimization loop with pre-allocated buffers:
>>> # Pre-compute static metadata once
>>> atom_ptr = ... # see Notes
>>> ext_atom_ptr = ...
>>> ext_batch_idx = ...
>>> N_ext = positions.shape[0] + 2 * alpha.shape[0]
>>> ext_vel = torch.empty(N_ext, 3, dtype=positions.dtype,
... device=positions.device)
>>> ext_forces = torch.empty_like(ext_vel)
>>> M = alpha.shape[0]
>>> vf = torch.empty(M, dtype=positions.dtype, device=positions.device)
>>> v_sumsq = torch.empty_like(vf)
>>> f_sumsq = torch.empty_like(vf)
>>> max_norm = torch.empty_like(vf)
>>> for step in range(num_steps):
... fire2_step_coord_cell(
... positions, velocities, forces,
... cell, cell_velocities, cell_force,
... batch_idx, alpha, dt, nsteps_inc,
... atom_ptr=atom_ptr,
... ext_atom_ptr=ext_atom_ptr,
... ext_velocities=ext_vel,
... ext_forces=ext_forces,
... ext_batch_idx=ext_batch_idx,
... vf=vf, v_sumsq=v_sumsq,
... f_sumsq=f_sumsq, max_norm=max_norm,
... )
"""
res = _coord_cell_mix_impl(
positions,
velocities,
forces,
cell,
cell_velocities,
cell_force,
batch_idx,
alpha,
dt,
nsteps_inc,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_velocities=ext_velocities,
ext_forces=ext_forces,
ext_batch_idx=ext_batch_idx,
vf=vf,
v_sumsq=v_sumsq,
f_sumsq=f_sumsq,
max_norm=max_norm,
delaystep=delaystep,
dtgrow=dtgrow,
dtshrink=dtshrink,
alphashrink=alphashrink,
alpha0=alpha0,
tmax=tmax,
tmin=tmin,
cell_force_scale=cell_force_scale,
compute_reductions=compute_reductions,
ext_positions=ext_positions,
)
if res is None:
return
(
wp_pos,
wp_vel,
wp_cell,
wp_cell_vel,
wp_dt,
wp_vf,
wp_ext_batch_idx,
wp_ext_atom_ptr,
wp_max_norm,
wp_device,
) = res
# Couple + clamp + apply the affine cell update directly on positions/cells.
_apply_fire2_coord_cell_step(
wp_pos,
wp_vel,
wp_cell,
wp_cell_vel,
wp_dt,
wp_vf,
wp_ext_batch_idx,
wp_ext_atom_ptr,
wp_max_norm,
maxstep=maxstep,
device=wp_device,
)
def fire2_step_coord_cell_mix(
positions: torch.Tensor,
velocities: torch.Tensor,
forces: torch.Tensor,
cell: torch.Tensor,
cell_velocities: torch.Tensor,
cell_force: torch.Tensor,
batch_idx: torch.Tensor,
alpha: torch.Tensor,
dt: torch.Tensor,
nsteps_inc: torch.Tensor,
*,
atom_ptr: torch.Tensor | None = None,
ext_atom_ptr: torch.Tensor | None = None,
ext_velocities: torch.Tensor | None = None,
ext_forces: torch.Tensor | None = None,
ext_batch_idx: torch.Tensor | None = None,
vf: torch.Tensor | None = None,
v_sumsq: torch.Tensor | None = None,
f_sumsq: torch.Tensor | None = None,
max_norm: torch.Tensor | None = None,
delaystep: int = 60,
dtgrow: float = 1.05,
dtshrink: float = 0.75,
alphashrink: float = 0.985,
alpha0: float = 0.09,
tmax: float = 0.08,
tmin: float = 0.005,
cell_force_scale: float = 1.0,
compute_reductions: bool = True,
) -> None:
r"""Phase 1 of the coupled variable-cell FIRE2 step: reduce + mix only.
Packs atomic + cell DOFs, runs the FIRE2 reduction (gated by
``compute_reductions``) and velocity mixing on the generalized DOFs, and
unpacks the mixed velocities back into ``velocities`` / ``cell_velocities``.
Updates ``alpha``, ``dt``, ``nsteps_inc`` in-place.
The reduction and mixing run over the generalized (atomic + cell) DOFs:
.. math::
P = \sum_i (\mathbf{v}_i + \Delta t\,\mathbf{F}_i) \cdot \mathbf{F}_i,\qquad
\mathbf{v} = (1-\alpha)\,\mathbf{v}
+ \left[(1-\alpha)\,\Delta t
+ \alpha\sqrt{\tfrac{\mathbf{v}\cdot\mathbf{v}}
{\mathbf{F}\cdot\mathbf{F}}}\right]\mathbf{F}
It does **not** apply the
position/cell step — pair it with :func:`fire2_step_coord_cell_couple` and
:func:`fire2_step_coord_cell_apply` so the displacement clamp can use a
``max_norm`` post-processed between the couple and apply phases.
See :func:`fire2_step_coord_cell` for parameter descriptions.
"""
_coord_cell_mix_impl(
positions,
velocities,
forces,
cell,
cell_velocities,
cell_force,
batch_idx,
alpha,
dt,
nsteps_inc,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_velocities=ext_velocities,
ext_forces=ext_forces,
ext_batch_idx=ext_batch_idx,
vf=vf,
v_sumsq=v_sumsq,
f_sumsq=f_sumsq,
max_norm=max_norm,
delaystep=delaystep,
dtgrow=dtgrow,
dtshrink=dtshrink,
alphashrink=alphashrink,
alpha0=alpha0,
tmax=tmax,
tmin=tmin,
cell_force_scale=cell_force_scale,
compute_reductions=compute_reductions,
ext_positions=None,
)
def fire2_step_coord_cell_couple(
positions: torch.Tensor,
velocities: torch.Tensor,
cell: torch.Tensor,
cell_velocities: torch.Tensor,
dt: torch.Tensor,
vf: torch.Tensor,
batch_idx: torch.Tensor,
max_norm: torch.Tensor,
*,
atom_ptr: torch.Tensor | None = None,
ext_atom_ptr: torch.Tensor | None = None,
ext_batch_idx: torch.Tensor | None = None,
) -> None:
r"""Phase 2 of the coupled variable-cell FIRE2 step: measure ``max_norm``.
Recomputes each atom's cell-coupled step from the mixed velocities (output
of :func:`fire2_step_coord_cell_mix`) and writes the per-system maximum
physical Cartesian displacement norm into ``max_norm`` (zeroed first):
.. math::
\text{max\_norm}[s] = \max_{i \in s} \lVert \Delta\mathbf{r}_i \rVert,
where :math:`\Delta\mathbf{r}_i = \Delta t\,\mathbf{v}_i` downhill and
:math:`-\tfrac{1}{2}\Delta t\,\mathbf{v}_i` uphill (:math:`vf[s] \le 0`), each
the coupled Cartesian atomic displacement including the affine cell remap.
Positions and cell are **not** moved, so a caller can post-process
``max_norm`` before :func:`fire2_step_coord_cell_apply`.
Parameters
----------
positions : torch.Tensor, shape (N, 3), dtype float32/float64
Atomic positions (read-only).
velocities : torch.Tensor, shape (N, 3), dtype float32/float64
Atomic velocities after the mix phase (read-only).
cell : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Cell matrices (read-only).
cell_velocities : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Cell velocity matrices after the mix phase (read-only).
dt : torch.Tensor, shape (M,), dtype float32/float64
Per-system timestep.
vf : torch.Tensor, shape (M,), dtype float32/float64
Per-system velocity-force dot product from the mix phase (read-only).
batch_idx : torch.Tensor, shape (N,), dtype int32
Sorted system index per atom.
max_norm : torch.Tensor, shape (M,), dtype float32/float64
Output buffer: zeroed then filled with the per-system maximum physical
Cartesian displacement norm. Modified in-place.
atom_ptr : torch.Tensor, shape (M+1,), dtype int32, optional
CSR-style atom pointers. Computed from *batch_idx* if ``None``.
ext_atom_ptr : torch.Tensor, shape (M+1,), dtype int32, optional
Extended atom pointers (2 cell DOFs per system). Computed if ``None``.
ext_batch_idx : torch.Tensor, shape (N+2M,), dtype int32, optional
System index for each element of the extended layout. Computed if ``None``.
"""
device = positions.device
M = dt.shape[0]
if positions.shape[0] == 0:
max_norm.zero_()
return
vec_type = _TORCH_TO_WP_VEC[positions.dtype]
mat_type = _TORCH_TO_WP_MAT[cell.dtype]
wp_device = wp.device_from_torch(device)
_, _, wp_ext_atom_ptr, wp_ext_batch_idx = _coord_cell_ext_metadata(
batch_idx,
M,
device,
wp_device,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_batch_idx=ext_batch_idx,
)
_fire2_coord_cell_compute_max_norm(
wp.from_torch(positions.detach(), dtype=vec_type),
wp.from_torch(velocities.detach(), dtype=vec_type),
wp.from_torch(cell.detach(), dtype=mat_type),
wp.from_torch(cell_velocities.detach(), dtype=mat_type),
wp.from_torch(dt.detach()),
wp.from_torch(vf),
wp_ext_batch_idx,
wp_ext_atom_ptr,
wp.from_torch(max_norm),
device=wp_device,
)
def fire2_step_coord_cell_apply(
positions: torch.Tensor,
velocities: torch.Tensor,
cell: torch.Tensor,
cell_velocities: torch.Tensor,
dt: torch.Tensor,
vf: torch.Tensor,
batch_idx: torch.Tensor,
max_norm: torch.Tensor,
*,
maxstep: float = 0.1,
atom_ptr: torch.Tensor | None = None,
ext_atom_ptr: torch.Tensor | None = None,
ext_batch_idx: torch.Tensor | None = None,
) -> None:
r"""Phase 3 of the coupled variable-cell FIRE2 step: clamp + apply.
Recomputes the same cell-coupled step as
:func:`fire2_step_coord_cell_couple`, clamps it by
:math:`\min(1, \text{maxstep}/\text{max\_norm}[s])` using the supplied
``max_norm``, and applies
.. math::
\mathbf{r} \leftarrow \mathbf{r}
+ \min\!\left(1, \tfrac{\text{maxstep}}{\text{max\_norm}[s]}\right)
\Delta\mathbf{r},
with :math:`\Delta\mathbf{r} = \Delta t\,\mathbf{v}` downhill and
:math:`-\tfrac{1}{2}\Delta t\,\mathbf{v}` uphill (:math:`vf[s] \le 0`). Writes
``positions``, ``cell``, ``cell_velocities`` (uphill zeroing), and the clamped
``dt`` in-place.
Parameters
----------
positions : torch.Tensor, shape (N, 3), dtype float32/float64
Atomic positions. Modified in-place.
velocities : torch.Tensor, shape (N, 3), dtype float32/float64
Atomic velocities after the mix phase (read-only; cell_velocities are
zeroed for uphill systems by the underlying Warp kernel).
cell : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Cell matrices. Modified in-place.
cell_velocities : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Cell velocity matrices. Modified in-place (zeroed for uphill systems).
dt : torch.Tensor, shape (M,), dtype float32/float64
Per-system timestep. Modified in-place (shrunk when step is clamped).
vf : torch.Tensor, shape (M,), dtype float32/float64
Per-system velocity-force dot product from the mix phase (read-only).
batch_idx : torch.Tensor, shape (N,), dtype int32
Sorted system index per atom.
max_norm : torch.Tensor, shape (M,), dtype float32/float64
Per-system maximum physical Cartesian displacement norm, as computed by
:func:`fire2_step_coord_cell_couple` (possibly post-processed by the
caller). Read-only.
maxstep : float, default 0.1
Maximum allowed displacement per atom. Steps larger than this are
rescaled by ``maxstep / max_norm[s]``.
atom_ptr : torch.Tensor, shape (M+1,), dtype int32, optional
CSR-style atom pointers. Computed from *batch_idx* if ``None``.
ext_atom_ptr : torch.Tensor, shape (M+1,), dtype int32, optional
Extended atom pointers (2 cell DOFs per system). Computed if ``None``.
ext_batch_idx : torch.Tensor, shape (N+2M,), dtype int32, optional
System index for each element of the extended layout. Computed if ``None``.
"""
device = positions.device
M = dt.shape[0]
if positions.shape[0] == 0:
return
vec_type = _TORCH_TO_WP_VEC[positions.dtype]
mat_type = _TORCH_TO_WP_MAT[cell.dtype]
wp_device = wp.device_from_torch(device)
_, _, wp_ext_atom_ptr, wp_ext_batch_idx = _coord_cell_ext_metadata(
batch_idx,
M,
device,
wp_device,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_batch_idx=ext_batch_idx,
)
_fire2_coord_cell_clamp_apply(
wp.from_torch(positions.detach(), dtype=vec_type),
wp.from_torch(velocities.detach(), dtype=vec_type),
wp.from_torch(cell.detach(), dtype=mat_type),
wp.from_torch(cell_velocities.detach(), dtype=mat_type),
wp.from_torch(dt.detach()),
wp.from_torch(vf),
wp_ext_batch_idx,
wp_ext_atom_ptr,
wp.from_torch(max_norm),
maxstep,
device=wp_device,
)
def fire2_compute_extended_reductions(
positions: torch.Tensor,
velocities: torch.Tensor,
forces: torch.Tensor,
cell: torch.Tensor,
cell_velocities: torch.Tensor,
cell_force: torch.Tensor,
batch_idx: torch.Tensor,
dt: torch.Tensor,
*,
atom_ptr: torch.Tensor | None = None,
ext_atom_ptr: torch.Tensor | None = None,
ext_velocities: torch.Tensor | None = None,
ext_forces: torch.Tensor | None = None,
ext_batch_idx: torch.Tensor | None = None,
cell_force_scale: float = 1.0,
) -> tuple[
tuple[torch.Tensor, torch.Tensor, torch.Tensor],
tuple[torch.Tensor, torch.Tensor, torch.Tensor],
]:
r"""Split the variable-cell FIRE2 reductions into atom and cell parts.
Computes the FIRE2 generalized-DOF reductions over the extended (atomic +
cell) degrees of freedom, using the same deferred :math:`\mathbf{v} +
\Delta t\,\mathbf{F}` half-step:
.. math::
\sum_i (\mathbf{v}_i + \Delta t\,\mathbf{F}_i) \cdot \mathbf{F}_i,\qquad
\sum_i \lVert \mathbf{v}_i + \Delta t\,\mathbf{F}_i \rVert^2,\qquad
\sum_i \mathbf{F}_i \cdot \mathbf{F}_i
Returns ``(atom_partial, cell_term)``, each a ``(vf, v_sumsq, f_sumsq)``
triple of ``(M,)`` tensors, such that ``atom_partial + cell_term`` equals the
reduction :func:`fire2_step_coord_cell_mix` computes over the packed
generalized DOFs. The atom part is a plain per-atom partition (combine it
across a caller-side partition), while the cell part is replicated (add it
exactly once)::
atom, cell_t = fire2_compute_extended_reductions(..., dt)
vf = allreduce(SUM, atom[0]) + cell_t[0] # and v_sumsq, f_sumsq
fire2_step_coord_cell_mix(..., vf=vf, ..., compute_reductions=False)
Uses the same ``v + f*dt`` half-step as the internal reduction, so the
combined result is bit-parity with recomputing it in one pass.
Parameters
----------
positions : torch.Tensor, shape (N, 3), dtype float32/float64
Atomic positions (read-only; used only to determine N and dtype).
velocities : torch.Tensor, shape (N, 3), dtype float32/float64
Atomic velocities (read-only).
forces : torch.Tensor, shape (N, 3), dtype float32/float64
Forces on atoms (read-only).
cell : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Cell matrices (read-only; used only to determine M).
cell_velocities : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Cell velocity matrices (read-only).
cell_force : torch.Tensor, shape (M, 3, 3), dtype float32/float64
Raw cell force matrices (read-only). Divided by
``atoms_per_system * cell_force_scale`` before packing.
batch_idx : torch.Tensor, shape (N,), dtype int32
Sorted system index per atom.
dt : torch.Tensor, shape (M,), dtype float32/float64
Per-system timestep used for the ``v + f*dt`` half-step projection.
atom_ptr : torch.Tensor, shape (M+1,), dtype int32, optional
CSR-style atom pointers. Computed from *batch_idx* if ``None``.
ext_atom_ptr : torch.Tensor, shape (M+1,), dtype int32, optional
Extended atom pointers (2 cell DOFs per system). Computed if ``None``.
ext_velocities : torch.Tensor, shape (N+2M, 3), optional
Scratch buffer for the packed extended velocity array. Allocated if ``None``.
ext_forces : torch.Tensor, shape (N+2M, 3), optional
Scratch buffer for the packed extended force array. Allocated if ``None``.
ext_batch_idx : torch.Tensor, shape (N+2M,), dtype int32, optional
System index for each element of the extended layout. Computed if ``None``.
cell_force_scale : float, default 1.0
Extra positive multiplier for cell-force normalization; cell forces are
divided by ``atoms_per_system * cell_force_scale``.
Returns
-------
atom_partial : tuple[torch.Tensor, torch.Tensor, torch.Tensor]
``(vf, v_sumsq, f_sumsq)`` reduction contributions from the atom DOFs
only, each of shape ``(M,)``. Sum across a caller-side partition before
combining with *cell_term*.
cell_term : tuple[torch.Tensor, torch.Tensor, torch.Tensor]
``(vf, v_sumsq, f_sumsq)`` reduction contributions from the cell DOFs
only, each of shape ``(M,)``. Replicated — add exactly once regardless
of the partition scheme.
See Also
--------
:func:`nvalchemiops.torch.fire2.fire2_step_coord_cell_mix` : Runs the mix phase with caller-supplied reductions.
"""
dtype = positions.dtype
device = positions.device
N = positions.shape[0]
M = cell.shape[0]
N_ext = N + 2 * M
vec_type = _TORCH_TO_WP_VEC[dtype]
mat_type = _TORCH_TO_WP_MAT[dtype]
wp_device = wp.device_from_torch(device)
if cell_force_scale <= 0.0:
raise ValueError("cell_force_scale must be positive")
def _triple():
return (
torch.zeros(M, dtype=dtype, device=device),
torch.zeros(M, dtype=dtype, device=device),
torch.zeros(M, dtype=dtype, device=device),
)
atom_partial = _triple()
cell_term = _triple()
if N == 0:
return atom_partial, cell_term
wp_dt = wp.from_torch(dt.detach())
wp_bidx = wp.from_torch(batch_idx.detach(), dtype=wp.int32)
# atom partial: the FIRE2 half-step reduction over the atom arrays directly.
fire2_reduce(
wp.from_torch(velocities.detach(), dtype=vec_type),
wp.from_torch(forces.detach(), dtype=vec_type),
wp_dt,
wp_bidx,
wp.from_torch(atom_partial[0]),
wp.from_torch(atom_partial[1]),
wp.from_torch(atom_partial[2]),
)
# total: pack the extended (atom + cell) DOFs and reduce over them.
if ext_velocities is None:
ext_velocities = torch.empty(N_ext, 3, dtype=dtype, device=device)
if ext_forces is None:
ext_forces = torch.empty(N_ext, 3, dtype=dtype, device=device)
wp_vel = wp.from_torch(velocities.detach(), dtype=vec_type)
wp_forces = wp.from_torch(forces.detach(), dtype=vec_type)
wp_cell_vel = wp.from_torch(cell_velocities.detach(), dtype=mat_type)
wp_ext_vel = wp.from_torch(ext_velocities, dtype=vec_type)
wp_ext_forces = wp.from_torch(ext_forces, dtype=vec_type)
atom_ptr, wp_atom_ptr, wp_ext_atom_ptr, wp_ext_batch_idx = _coord_cell_ext_metadata(
batch_idx,
M,
device,
wp_device,
atom_ptr=atom_ptr,
ext_atom_ptr=ext_atom_ptr,
ext_batch_idx=ext_batch_idx,
)
atom_counts = atom_ptr[1:] - atom_ptr[:-1]
if torch.any(atom_counts <= 0).item():
raise ValueError(
"fire2_compute_extended_reductions requires at least one atom per system"
)
cell_force_divisor = atom_counts.to(dtype=dtype).reshape(M, 1, 1) * cell_force_scale
cell_force_work = (cell_force.detach() / cell_force_divisor).contiguous()
wp_cell_force = wp.from_torch(cell_force_work, dtype=mat_type)
if M == 1:
pack_velocities_with_cell(wp_vel, wp_cell_vel, wp_ext_vel, device=wp_device)
pack_forces_with_cell(wp_forces, wp_cell_force, wp_ext_forces, device=wp_device)
else:
pack_velocities_with_cell(
wp_vel,
wp_cell_vel,
wp_ext_vel,
wp_atom_ptr,
wp_ext_atom_ptr,
device=wp_device,
batch_idx=wp_bidx,
)
pack_forces_with_cell(
wp_forces,
wp_cell_force,
wp_ext_forces,
wp_atom_ptr,
wp_ext_atom_ptr,
device=wp_device,
batch_idx=wp_bidx,
)
total = _triple()
fire2_reduce(
wp_ext_vel,
wp_ext_forces,
wp_dt,
wp_ext_batch_idx,
wp.from_torch(total[0]),
wp.from_torch(total[1]),
wp.from_torch(total[2]),
)
# cell term = total (atom + cell) - atom partial.
for i in range(3):
cell_term[i].copy_(total[i] - atom_partial[i])
return atom_partial, cell_term
[docs]
def fire2_step_extended(
ext_positions: torch.Tensor,
ext_velocities: torch.Tensor,
ext_forces: torch.Tensor,
ext_batch_idx: torch.Tensor,
alpha: torch.Tensor,
dt: torch.Tensor,
nsteps_inc: torch.Tensor,
*,
vf: torch.Tensor | None = None,
v_sumsq: torch.Tensor | None = None,
f_sumsq: torch.Tensor | None = None,
max_norm: torch.Tensor | None = None,
delaystep: int = 60,
dtgrow: float = 1.05,
dtshrink: float = 0.75,
alphashrink: float = 0.985,
alpha0: float = 0.09,
tmax: float = 0.08,
tmin: float = 0.005,
maxstep: float = 0.1,
) -> None:
r"""Run FIRE2 directly on pre-packed extended arrays (no pack/unpack).
This is a lower-level API for callers that maintain persistent extended
arrays (positions + cell DOFs interleaved). The caller is responsible
for packing data into the extended layout before the first call and
unpacking results after the last call (or as needed).
Runs the standard FIRE2 scheme directly on the packed generalized DOFs
:math:`\mathbf{v}`, :math:`\mathbf{F}`:
.. math::
P = \sum_i (\mathbf{v}_i + \Delta t\,\mathbf{F}_i) \cdot \mathbf{F}_i,\qquad
\mathbf{v} = (1-\alpha)\,\mathbf{v}
+ \left[(1-\alpha)\,\Delta t
+ \alpha\sqrt{\tfrac{\mathbf{v}\cdot\mathbf{v}}
{\mathbf{F}\cdot\mathbf{F}}}\right]\mathbf{F}
with displacement :math:`\Delta\mathbf{r} = \Delta t\,\mathbf{v}` downhill and
:math:`-\tfrac{1}{2}\Delta t\,\mathbf{v}` uphill (:math:`P \le 0`, velocity
then zeroed), capped by
:math:`\min(1, \text{maxstep}/\lVert\Delta\mathbf{r}\rVert)` per system with
:math:`\Delta t` scaled by the same factor.
This eliminates the per-step pack/unpack overhead that
``fire2_step_coord_cell`` incurs.
Unlike :func:`fire2_step_coord_cell`, this function treats the packed DOFs
exactly as provided. It does not add the affine atomic remap implied by
variable-cell motion, so it should be used only when the caller explicitly
wants generic packed-DOF FIRE2 behavior.
Parameters
----------
ext_positions : torch.Tensor, shape (N_ext, 3)
Extended position array (atoms + cell DOFs interleaved).
ext_velocities : torch.Tensor, shape (N_ext, 3)
Extended velocity array.
ext_forces : torch.Tensor, shape (N_ext, 3)
Extended force array.
ext_batch_idx : torch.Tensor, shape (N_ext,), dtype=int32
System index for each element in the extended arrays.
alpha : torch.Tensor, shape (M,)
FIRE2 mixing parameter per system.
dt : torch.Tensor, shape (M,)
Timestep per system.
nsteps_inc : torch.Tensor, shape (M,), dtype=int32
Consecutive positive-power step counter per system.
vf, v_sumsq, f_sumsq, max_norm : torch.Tensor or None
Per-system scratch buffers, shape (M,). Allocated internally if None.
delaystep, dtgrow, dtshrink, alphashrink, alpha0, tmax, tmin, maxstep :
FIRE2 hyperparameters. See ``fire2_step_coord_cell`` for details.
Notes
-----
Modifies ``ext_positions``, ``ext_velocities``, ``alpha``, ``dt``,
and ``nsteps_inc`` in-place.
"""
dtype = ext_positions.dtype
device = ext_positions.device
M = alpha.shape[0]
vec_type = _TORCH_TO_WP_VEC[dtype]
# Reduction scratch buffers
vf = _alloc_or_zero(vf, M, dtype, device)
v_sumsq = _alloc_or_zero(v_sumsq, M, dtype, device)
f_sumsq = _alloc_or_zero(f_sumsq, M, dtype, device)
max_norm = _alloc_or_zero(max_norm, M, dtype, device)
if ext_positions.shape[0] == 0:
return
fire2_step(
wp.from_torch(ext_positions.detach(), dtype=vec_type),
wp.from_torch(ext_velocities.detach(), dtype=vec_type),
wp.from_torch(ext_forces.detach(), dtype=vec_type),
wp.from_torch(ext_batch_idx.detach(), dtype=wp.int32),
wp.from_torch(alpha.detach()),
wp.from_torch(dt.detach()),
wp.from_torch(nsteps_inc.detach(), dtype=wp.int32),
wp.from_torch(vf),
wp.from_torch(v_sumsq),
wp.from_torch(f_sumsq),
wp.from_torch(max_norm),
delaystep=delaystep,
dtgrow=dtgrow,
dtshrink=dtshrink,
alphashrink=alphashrink,
alpha0=alpha0,
tmax=tmax,
tmin=tmin,
maxstep=maxstep,
)