Implicit differentiationยค
Gradients of converged quantities: implicit_density exposes the SCF fixed
point as a differentiable function of the energy functional (CPHF as a
custom_vjp), so a polarizability is one jax.jacobian through the converged
density, with no field stepping.
import jax; jax.config.update("jax_enable_x64", True)
import equinox as eqx
import jax.numpy as jnp
import numpy as np
from dftax import KS, Molecule, becke, implicit_density, polarizability
from dftax.integrals.multipole import dipole_matrices
from dftax.ks.properties import nuclear_dipole
from dftax.energy.xc import PBE
mol = Molecule.from_xyz("O 0 0 0; H 0.7586 0 0.5043; H 0.7586 0 -0.5043", "sto-3g")
ks0 = KS(mol, PBE(), grid=becke(50, 110))
D = dipole_matrices(ks0.basis)
nuc = nuclear_dipole(mol)
def mu(field):
ksf = eqx.tree_at(lambda k: k.hcore, ks0, ks0.hcore + jnp.einsum("i,ipq->pq", field, D))
return nuc - jnp.einsum("ipq,pq->i", D, implicit_density(ksf))
alpha = jax.jacobian(mu)(jnp.zeros(3))
alpha = 0.5 * (alpha + alpha.T)
print("analytic polarizability (a.u.):\n", np.asarray(alpha))
analytic polarizability (a.u.):
[[ 3.63690551e+00 -4.55636293e-16 -4.14868558e-13]
[-4.55636293e-16 1.24682744e-02 -5.52033694e-16]
[-4.14868558e-13 -5.52033694e-16 2.74677412e+00]]
alpha_fd = np.asarray(polarizability(mol, PBE(), method="fd", grid=becke(50, 110)))
print("finite-field reference:\n", alpha_fd)
print("max |diff| =", float(np.abs(np.asarray(alpha) - alpha_fd).max()))
finite-field reference:
[[ 3.63690617e+00 -1.92407201e-09 5.34711164e-10]
[-1.92407201e-09 1.24688180e-02 -2.12468931e-11]
[ 5.34711164e-10 -2.12468931e-11 2.74675871e+00]]
max |diff| = 1.5413626930360635e-05