Skip to content

Analytic nuclear forces¤

Forces are F_A = −∂E/∂R_A, obtained in one reverse-mode pass by differentiating the total energy through the whole geometry-dependent pipeline: the basis centers follow their atoms, the integrals are differentiable in those centers, and the Becke grid moves with the nuclei. The density is held at the converged solution via a Löwdin parametrization, so at the SCF stationary point the gradient reduces to the explicit geometry derivative, capturing both the Hellmann-Feynman and the Pulay terms with no hand-coded integral derivatives. (This is the envelope theorem; differentiating through the SCF loop is unnecessary for forces.) A result converged under fermi() smearing is handled automatically: the full fractional-occupation density is frozen instead of an integer projector, and the reported force is the gradient of the Mermin free energy. Any dispersion=d3bj() / d4() term differentiates along with the rest.

import jax; jax.config.update("jax_enable_x64", True)
import numpy as np
from dftax import KS, Molecule, becke, forces, scf
from dftax.energy.xc import PBE

mol = Molecule.from_xyz("H 0 0 0; H 0 0 0.74", "sto-3g")
grid = becke(n_radial=50, lebedev=110)
res = scf(KS(mol, PBE(), grid=grid))
F = np.asarray(forces(mol, PBE(), res, grid=grid))
print(F)                       # (n_atom, 3) Ha/Bohr
print("net |Σ F| =", np.abs(F.sum(0)).max())   # ~0 (translational invariance)

forces(mol, xc, res, *, grid=..., coulomb=None) takes the converged KSResult (from scf or minimize) and slices the per-channel occupied orbitals itself; open shells work through the same call (you can also pass the occupied coefficients directly). Pass the same becke(...) grid spec as the energy calculation; explicit point grids cannot follow the nuclei, so only Becke specs are accepted. With coulomb=df("...") the forces are for the density-fitted energy surface. The default chunk="auto" materializes the 3-center tensor when it fits the memory budget and otherwise streams the geometry gradient over auxiliary slabs (hybrid exchange through a frozen-orbital RI-K, fully differentiable; range-separated hybrids stream both exchange channels); an int chunk forces streaming. screen= streams too: the Schwarz shell-pair selection is resolved at the reference geometry and baked into the contraction plans, so every DF configuration carries geometry gradients.

A finite-difference check (central difference of the energy) agrees with the analytic force to ~1e-9; see examples/03_forces_h2.py.

For forces over many geometries at once, see batched evaluation.