Status: design note (not yet implemented). Captures the intended approach for making the engine differentiable so it can serve as a data and gradient engine for machine-learned interatomic potentials, inverse design, and learned exchange-correlation functionals.
The engine is already partially differentiable: forces are −dE/dR and the stress tensor is dE/dε, both computed analytically. "Making it differentiable" means generalizing that existing machinery into a uniform gradient API and exposing it to ML toolchains.
The engineering cost is dominated by which regime you target.
The engine runs as a batch job: for each structure, return (energy, forces, ∂E/∂θ). A single host (CPU↔GPU) boundary per full SCF solve is negligible against the cost of the solve itself.
(E, grad) batch API and/or a PyTorch torch.autograd.Function backed by a C++/CUDA op.The engine is invoked repeatedly inside a training/optimization graph (inverse design, in-the-loop functional learning). Here fine-grained host roundtrips are catastrophic and the binding/memory traps in §4 all apply.
Recommendation: build the adjoint math (§2) first — it is required in both regimes — then ship Regime A. Only build the on-device JAX path (§4) when a concrete Regime B workload justifies it.
Unrolling the self-consistent iterations and reverse-mode AD-ing through them is a trap (memory blow-up, numerical noise, differentiates the solver not the solution).
The SCF solution satisfies n* = F(n*, θ). Differentiate the converged fixed point, not the iteration:
Solve one linear system at convergence, reusing the existing mixing / Krylov machinery to apply (I − ∂F/∂n)^(-1). This is less code and more stable than differentiating each step.
At self-consistency the energy is variational, so wavefunction-response terms vanish (Hellmann–Feynman):
The plane-wave basis is position-independent, so atomic-position gradients carry no Pulay/basis-derivative term (already exploited for forces). Generalize this from "θ = atomic position" to "θ = any parameter".
Needed for phonons, elastic constants, Hessian-based quantities. Solve the DFPT / Sternheimer response equation:
projected onto the unoccupied subspace.
Each energy term exposes value and a hand-coded vector–Jacobian product. This maps onto the existing calculator decomposition (each *Calculator gains a backward() / vjp() alongside compute()).
Difficulty is not uniform:
vxc (1st), fxc (2nd), kxc (3rd) functional derivatives.1/(ε_i − ε_j) terms; guard against near-degeneracy with degenerate perturbation theory.degauss > 0 when computing parameter gradients on metals.Fixed/discrete inputs (k-grid, symmetry reduction, G-vector cutoff) are held constant across a gradient evaluation.
Forward vs reverse mode by parameter count:
dE/dR on a small cell): forward mode / dual numbers — simplest, memory-light.Build both; route on dim(θ).
The C++ core owns the math (forward + adjoints). The Python layer registers the engine as a custom differentiable op.
torch.autograd.Function + a custom C++/CUDA extension works via standard bindings.
nanobind + jax.custom_vjp wrapping a host function behaves like a pure_callback: JAX pulls tensors off GPU VRAM, runs the C++ on CPU, and copies back — a catastrophic I/O bottleneck for fine-grained ops.
Fix: lower the C++ entry points into XLA via **jax.ffi** (the current foreign-function interface, superseding hand-registered custom_call targets) so the compiled graph stays on the accelerator. Caveat: jax.ffi only helps if the underlying kernel is itself on-device (CUDA/HIP/SYCL) — FFI alone does not remove a CPU-only kernel's roundtrip.
When data crosses the C++/Python boundary, copying multi-GB wavefunction arrays will dominate runtime. Use DLPack (supported by nanobind) to share GPU buffers across PyTorch / JAX / CuPy without reallocation; use the buffer / ndarray protocol on CPU.
EnergyTerm interface — no new physics, just structure.dE/dR against current analytic forces (match to machine precision).fxc + adjoints.(E, grad) batch API and/or PyTorch autograd.Function.jax.ffi + DLPack on-device path.Steps 1–2 are low-risk and also harden forces. Steps 3–4 unlock the "data _and_ gradient engine for ML potentials" differentiator.
The approach above is written for the current plane-wave representation. If the engine later adds other discretizations (real-space grids, local-orbital / LCAO DFT), most of the machinery carries over — but it bakes in a few plane-wave assumptions that must be made explicit.
So layer 4 (computation) and the binding layers survive a basis change largely intact. **The changes live in layer 3 (representation) and in how layer 5 (properties) assembles gradients.**
Plane waves e^(iG·r) do not move with the atoms, so atomic-position gradients have no basis-derivative term — pure Hellmann–Feynman. The note relies on this. The general form is:
Note the plane-wave basis already depends on the cell (G-vectors and the cutoff sphere scale with the reciprocal lattice) — which is exactly why stress already needs finite-difference dV/dG handling. The EnergyTerm / operator interface must therefore treat ∂/∂θ as a general, possibly-nonzero quantity supplied by the representation: zero for plane-wave position gradients, nonzero otherwise.
operator.apply(). This is the most important code change.The largest departure: the basis is attached to atoms, φ_μ(r − R_A).
S(θ) and Hamiltonian H(θ) depend on atomic positions through the basis functions. Position gradients require differentiating the basis integrals (overlap, kinetic, nuclear attraction, Hartree/XC matrix elements). Analytic for Gaussians; radial-spline derivatives for numerical atomic orbitals.Hψ = εSψ with position-dependent S — structurally the same machinery already built for ultrasoft. The US "overlap operator + `∂S/∂θ`" work is the direct stepping stone; generalize it from US augmentation to the basis overlap.Tr(W ∂S/∂θ), which maps directly onto a VJP.operator.apply() (plus the mixer as preconditioner); never on FFT-diagonal structure. This is what lets real-space and LCAO reuse the same implicit-diff code.In short: §2 and §4 carry over unchanged; the adjustments are (a) make Pulay / basis-derivatives first-class, (b) make the response solver discretization-agnostic, and (c) expect the hard adjoint work to migrate from eigensolver degeneracy (plane waves) toward basis-integral derivatives (LCAO) or discretization-noise control (real-space). The ultrasoft overlap-derivative work is the natural bridge.