4.14. Custom functionals 1ΒΆ

This example gives an example of how eminus can be extended at runtime, e.g., by using custom functionals. Also, this example outlines how one can parameterize their own LDA functional.


import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import curve_fit

import eminus.xc
from eminus import Atoms, SCF
from eminus.units import ry2ha

Start with a normal SCF calculation for helium with the original LDA Chachiyo functional

atoms = Atoms("He", (0, 0, 0), ecut=10, verbose=0)
scf = SCF(atoms, xc="lda,chachiyo")
scf.run()
print(f"Energies with the Chachiyo functional:\n{scf.energies}")

Define arrays for the Quantum Monte Carlo (QMC) results from Ceperley and Alder for the homogeneous electron gas

The values are the paramagnetic correlation energies from Tab. 5 in Can. J. Phys. 58, 1200

rs = np.array([2, 5, 10, 20, 50, 100])
# Convert from -mRy to Hartree
ecp = -ry2ha(np.array([90.2, 56.3, 37.22, 23.00, 11.40, 6.379])) / 1000

Define the original Chachiyo functional parameters

The parameters are derived such that the functional recovers the values in Phys. Rev. B 84, 033103 for the high-density limit

Karasiev re-parameterized the functional in J. Chem. Phys. 145, 157101 without the restriction of b=c

Here, c uses the same value as in Chachiyo to preserve the correct high-density limit but b is chosen such that it recovers the exact value for rs=50

We will now build a new custom functional by fitting b over the QMC data set

a = (np.log(2) - 1) / (2 * np.pi**2)
c = 20.4562557
b = c
print(f"\nOriginal parameter:\nb = {b:.7f}")

All functionals in eminus share the same signature and return types

Define a custom spin-paired functional using said signature based on the Chachiyo functional but with the customizable parameter parameter b so we can fit over it

def custom_functional(n, b, **kwargs):
    rs = (3 / (4 * np.pi * n)) ** (1 / 3)  # Wigner-Seitz radius
    ec = a * np.log(1 + b / rs + c / rs**2)  # Exchange energy
    vc = ec + a * (2 * c + b * rs) / (3 * (c + b * rs + b * rs**2))  # Exchange potential
    return ec, np.array([vc]), None

Fit the functional using a wrapper function

We only fit over the energies so the wrapper function only returns the first argument of the functional

The third return value of the function (here None) is only used in GGA functionals

Functionals usually take densities as the input, so convert the Wigner-Seitz radius to a density in the wrapper function

def functional_wrapper(rs, b):
    n = 3 / (4 * np.pi * rs**3)  # Density from Wigner-Seitz radius
    return custom_functional(n, b)[0]

Do the fit over the QMC data

The resulting parameter still recovers the limits of the original functional but can be more accurate in the mid-density regimes

One can see that our fitted value is similar to the one derived by Karasiev with b=21.7392245

fitted_b, _ = curve_fit(functional_wrapper, rs, ecp)
print(f"\nFitted parameter:\nb = {fitted_b[0]:.7f}")
# Fitted parameter:
# b = 21.9469106

Plot the error of the correlation energies compared to the QMC data for Chachiyo, Karasiev, and our functional

Find the plot named correlation_energy_error.png

../_images/correlation_energy_error.png
plt.style.use("../eminus.mplstyle")
plt.figure()
plt.axhline(c="dimgrey", ls="--", marker="")
plt.plot(rs, 1000 * (functional_wrapper(rs, b) - ecp), label="Chachiyo")
plt.plot(rs, 1000 * (functional_wrapper(rs, 21.7392245) - ecp), label="Karasiev")
plt.plot(rs, 1000 * (functional_wrapper(rs, *fitted_b) - ecp), label="mod. Chachiyo")
plt.xlabel(r"$r_s$")
plt.ylabel(r"$E_c^\mathrm{P} - E_c^{\mathrm{P,QMC}}$ [m$E_\mathrm{h}$]")
plt.legend()
plt.savefig("correlation_energy_error.png")

Some modules of eminus have an IMPLEMENTED lookup dictionary for functions that can be extended

These are available in the xc, potentials, and minimizer modules and can be used as seen below

Plug the fitted parameters into the custom functional

eminus.xc.IMPLEMENTED["custom"] = lambda n, **kwargs: custom_functional(n, *fitted_b)

If the signature of the custom functional matches the signature from eminus it is sufficient to write

# eminus.xc.IMPLEMENTED["custom"] = custom_functional

Start an SCF calculation with our custom functional

scf = SCF(atoms, xc="lda,custom")
scf.run()
print(f"\nEnergies with the fitted Chachiyo functional:\n{scf.energies}")

An extension of this example for the spin-polarized case can be found in the next example

Download 14_custom_functionals.py correlation_energy_error.png