Optimization and Integrators#
This page covers the concrete simulation types provided by the dynamics module.
All of them follow the execution loop described in the dynamics
overview — they generally differ only in what pre_update and post_update do.
Geometry optimization#
Geometry optimization finds the nearest local energy minimum by iteratively moving atoms downhill on the potential energy surface. The toolkit provides the FIRE (Fast Inertial Relaxation Engine) algorithm in two variants.
Fixed-cell optimization#
FIRE optimizes atomic positions
while keeping the simulation cell fixed:
from nvalchemi.dynamics import FIRE, ConvergenceHook
with FIRE(
model=model,
dt=0.1, # initial timestep (femtoseconds)
n_steps=500,
hooks=[ConvergenceHook(fmax=0.05)],
) as opt:
relaxed = opt.run(batch)
FIRE uses an adaptive timestep and velocity mixing: when the system is moving downhill (forces aligned with velocities), the timestep grows and velocities are biased toward the force direction. When the system overshoots, the timestep shrinks and velocities are zeroed. This makes it robust across a wide range of systems without manual tuning.
Variable-cell optimization#
FIREVariableCell extends FIRE to
simultaneously optimize both atomic positions and the simulation cell. This is
useful for finding equilibrium crystal structures where the lattice parameters are
not known a priori:
from nvalchemi.dynamics.optimizers.fire import FIREVariableCell
from nvalchemi.dynamics import ConvergenceHook
with FIREVariableCell(
model=model,
dt=0.1,
n_steps=500,
hooks=[ConvergenceHook(fmax=0.05)],
) as opt:
relaxed = opt.run(batch)
The cell degrees of freedom are propagated using an NPH-like scheme at zero target
pressure. The model must return stresses (or virials) in addition to forces.
Choosing between fixed and variable cell#
Use fixed-cell FIRE when the cell is known (e.g. a bulk crystal at experimental lattice parameters, or a molecule in vacuum where the cell is just a bounding box). Use variable-cell FIRE when the equilibrium cell shape or volume is unknown, such as when screening candidate crystal structures or computing equations of state.
Molecular dynamics#
Molecular dynamics (MD) propagates the equations of motion forward in time, sampling the trajectory of a system at finite temperature. The toolkit provides integrators for three standard ensembles.
NVE: energy conservation#
NVE uses the Velocity Verlet
algorithm — a symplectic integrator that conserves total energy in the
microcanonical ensemble:
from nvalchemi.dynamics import NVE
with NVE(model=model, dt=1.0, n_steps=1000) as md:
trajectory = md.run(batch)
NVE is the natural choice for verifying that a model’s energy surface is smooth enough for stable dynamics: if the total energy drifts significantly, the force field is likely too noisy for the chosen timestep.
NVT: constant temperature#
NVTLangevin implements the
BAOAB Langevin splitting scheme, which samples the canonical (NVT) ensemble exactly
— the thermostat does not introduce systematic bias:
from nvalchemi.dynamics import NVTLangevin
with NVTLangevin(
model=model,
dt=1.0, # femtoseconds
temperature=300.0, # Kelvin
friction=0.01, # collision frequency (1/fs)
n_steps=10000,
) as md:
trajectory = md.run(batch)
The friction parameter controls how strongly the thermostat couples to the
system. A low value gives longer correlation times (closer to NVE); a high value
thermalises quickly but damps real dynamics.
NPT: constant pressure#
NPT uses the
Martyna–Tobias–Klein (MTK) barostat with Nose–Hoover chains to sample the
isothermal-isobaric ensemble. Both the atomic positions and the simulation cell
evolve:
from nvalchemi.dynamics import NPT
with NPT(
model=model,
dt=1.0,
temperature=300.0,
pressure=1.0, # target pressure (bar)
n_steps=10000,
) as md:
trajectory = md.run(batch)
The model must return stresses for NPT to propagate the cell degrees of freedom.
Writing your own dynamics#
All integrators and optimizers inherit from
BaseDynamics. To implement a custom one, you
subclass it and override pre_update and post_update — the two methods that
define how the batch state evolves within a single step.
The minimal contract#
Your subclass must provide:
__needs_keys__— a set of strings naming the model outputs your dynamics reads (e.g.{"forces"}, or{"forces", "stresses"}for cell-aware schemes).__provides_keys__— a set of strings naming the batch keys your dynamics writes (e.g.{"positions", "velocities"}).pre_update(batch)— called before the model forward pass. Typically updates positions using current velocities and/or forces.post_update(batch)— called after the model forward pass. Typically completes the velocity update with the newly computed forces.
Both methods receive the Batch and modify it
in-place. Return value is None.
Example: a Velocity Verlet integrator#
The DemoDynamics class in nvalchemi.dynamics.demo is a complete, minimal
Velocity Verlet implementation that is useful as a template:
from nvalchemi.data import Batch
from nvalchemi.dynamics.base import BaseDynamics, ConvergenceHook
class MyVerlet(BaseDynamics):
__needs_keys__ = {"forces"}
__provides_keys__ = {"positions", "velocities"}
def __init__(self, model, n_steps, dt=1.0, hooks=None, convergence_hook=None, **kwargs):
super().__init__(
model=model, hooks=hooks, convergence_hook=convergence_hook,
n_steps=n_steps, **kwargs,
)
self.dt = dt
self._prev_accelerations = None
def pre_update(self, batch: Batch) -> None:
"""Position half-step: x(t+dt) = x(t) + v*dt + 0.5*a*dt^2."""
import torch
positions = batch.positions
velocities = batch.velocities
forces = batch.forces
masses = batch.atomic_masses.unsqueeze(-1)
with torch.no_grad():
if forces is not None and not torch.all(forces == 0):
acc = forces / masses
self._prev_accelerations = acc.clone()
positions.add_(velocities * self.dt + 0.5 * acc * self.dt**2)
else:
positions.add_(velocities * self.dt)
def post_update(self, batch: Batch) -> None:
"""Velocity half-step: v(t+dt) = v(t) + 0.5*(a_old + a_new)*dt."""
import torch
velocities = batch.velocities
forces = batch.forces
masses = batch.atomic_masses.unsqueeze(-1)
with torch.no_grad():
new_acc = forces / masses
if self._prev_accelerations is not None:
velocities.add_(0.5 * (self._prev_accelerations + new_acc) * self.dt)
else:
velocities.add_(new_acc * self.dt)
Important
The demo VelocityVerlet class is intended for debugging and pedagogy
only. Do not use this class for production runs, and instead, see the
NVE class instead.
Data flow through a step#
Understanding what the batch contains at each point is key to writing correct updates:
Point in step |
What just happened |
What the batch contains |
|---|---|---|
|
Hooks ran |
Positions and velocities from the previous step; forces may be from the previous |
|
You updated positions |
New positions; velocities partially updated (or unchanged) |
After |
Model ran |
Fresh |
|
Forces are fresh |
Complete the velocity update with new forces |
|
Step is done |
Consistent positions, velocities, and forces for the current timestep |
Gotchas and tips#
Use
torch.no_grad(): Wrap in-place updates intorch.no_grad()to avoid conflicts with autograd. Whenforces_via_autograd=True,compute()setsrequires_grad_(True)on positions to compute forces via backprop.In-place operations: Modify batch tensors in-place (
positions.add_(...)) rather than reassigning. The batch’s storage model expects tensors to be updated in place.First-step fallback: On the first call to
pre_update, forces may beNoneor zero (no model evaluation has happened yet). Guard against this and fall back to an Euler step.Per-system state: If your integrator needs auxiliary state (e.g. thermostat variables, previous accelerations), store it as instance attributes. The
_prev_accelerationspattern above is typical.__needs_keys__matters:BaseDynamicsuses this set to verify the model produces the required outputs before the simulation starts. If your dynamics needs stresses, declare{"forces", "stresses"}.FusedStage compatibility: When your dynamics runs inside a
FusedStage, a save-and-restore mask is applied aroundpre_updateandpost_updateso that only systems belonging to your stage are modified. You do not need to handle masking yourself.
See also#
Overview: The Dynamics overview describes the shared execution loop and multi-stage pipelines.
Hooks: The Hooks guide covers convergence criteria, logging, and snapshots.
Examples:
02_dynamics_example.pydemonstrates a complete relaxation and MD workflow.