Towards Routine Orbital-Free Large-Scale Quantum-Mechanical Modelling of Materials


Last week, I got to participate in a workshop Towards Routine Orbital-free Large-Scale Quantum-Mechanical Modelling of Materials held in Hangzhou, China. Orbital-Free Density Functional Theory (OF-DFT) is an attractive alternative to the far more popular Kohn-Sham Density Functional Theory:

$$\begin{equation} E[\rho] = T_s[\rho] + E_H[\rho] + E_{ext}[\rho] + \int v_{ext}\rho(r)dr \end{equation}$$

Here $T_s$ is the kinetic energy density functional - deriving approximations to this term is the hardest part of OF-DFT development. At the workshop, one of the speakers presented their own OF-DFT package: DFTpy. Experimenting with this software could be a good way to spend this blog post.

But first, photos of China.

Lake from the Conference Trees around the Venue Museum near the Lake Night View

Calculation Setup

DFTpy already has an ASE calculator, so we can use that as a base for comparison to our Kohn-Sham BigDFT code.

The calculations (especially with BigDFT) are non-trivial so let's setup remotemanager to do this on my local cluster.

from remotemanager import RemoteFunction, SanzuFunction
from spring import Spring
url = Spring()
url.mpi = 4
url.omp = 9
url.queue = "winter2"
url.conda = "bdft"
url.path_to_bigdft = "/home/dawson/binaries/bdft/"

First, make a system of bulk magnesium.

A = 3.2091 # taken from Wikipedia
C = 5.2103
@RemoteFunction
def make_ase(a=A, c=C):
    from ase.build import bulk
    asys = bulk("Mg", crystalstructure="hcp", orthorhombic=True, 
                a=a, c=c)
    return asys

Visualize this using PyBigDFT.

@RemoteFunction
def make_system(a=A, c=C):
    from BigDFT.Systems import System
    from BigDFT.UnitCells import UnitCell
    from BigDFT.Interop.ASEInterop import ase_to_bigdft, \
         ase_cell_to_bigdft

    # Build with ASE
    asys = make_ase(a, c)

    # Convert to BigDFT
    sys = System()
    sys["FRA:0"] = ase_to_bigdft(asys)
    sys.cell = ase_cell_to_bigdft(asys.cell)

    return sys
sys = make_system()
_ = sys.display()

3Dmol.js failed to load for some reason. Please check your browser console for error messages.

Using BigDFT, we can compute a potential energy curve by varying the lattice vectors. Let's try varying a.

from numpy import linspace
avals = linspace(3, 3.4, 11)
@SanzuFunction(url=url, name="bigdft", verbose=False)
def calculate(a, pts, name):
    from BigDFT.Inputfiles import Inputfile
    from BigDFT.Calculators import SystemCalculator

    calc = SystemCalculator(skip=True)
    sys = make_system(a=a)

    inp = Inputfile()
    inp.set_xc("LDA")
    inp.set_hgrid(0.35)
    inp.set_psp_krack(functional="LDA")
    inp["kpt"] = {"method": "mpgrid", "ngkpt": pts}
    inp["import"] = "mixing" # because it's a metal

    log = calc.run(sys=sys, input=inp, name=name, run_dir="scr")
    return log.energy
results = {}
for i, a in enumerate(avals):
    results[a] = calculate(a, [6, 4, 4], f"bigdft-{i}")

Plot the change in energy per unit cell.

def plot_all(data):
    from matplotlib import pyplot as plt
    fig, axs = plt.subplots(1, 1, dpi=150, figsize=(5, 3))
    for k, v in data.items():
        minv = min(v.values())
        axs.plot(avals, [27.2114 * (v[x] - minv) for x in avals], 
                 'o--', label=k)
    axs.set_ylabel("Energy Difference (eV / cell)")
    axs.set_xlabel("Lattice Vector a ($\mathring{A}$)")
    axs.legend()
plot_all({"BigDFT": results})

energy curve of BigDFT

Orbital-Free Comparison

Ok now let's try to do the same thing with OF-DFT and DFTpy. To do this, we're going to need a local pseudopotential for use with OF-DFT. We can use the BLPS library of pseudopotentials for this test.

from os import system
pp = "https://raw.githubusercontent.com/PrincetonUniversity/BLPSLibrary/master/LDA/reci/mg.lda.recpot"
_ = system(f"mkdir -p pp; cd pp; wget {pp} 2>/dev/null")

And prepare to run DFTpy.

# mpi parallel version is also available
url.omp = 1
url.mpi = 1
@SanzuFunction(url=url, name="of-dft", verbose=False, extra_files_send="pp")
def calculate_of(a, pts, name, kedf):
    from ase.io import write
    from futile.Utils import ensure_dir
    from shutil import copyfile
    from os import system

    # no built in toml writer...
    def dump(inp, ofile):
        for h, ent in inp.items():
            ofile.write(f"[{h}]\n")
            for k, v in ent.items():
                ofile.write(f"{k}={v}\n")

    # create the system
    asys = make_ase(a=a)
    asys = asys * pts  # build the supercell instead of k-points

    # write the files to the scratch directory
    ensure_dir("scr-of")
    write(f"scr-of/{name}.vasp", asys)
    copyfile("pp/mg.lda.recpot", "scr-of/mg.lda.recpot")

    # create the input file
    inp = {}
    inp["PP"] = {"Mg": "mg.lda.recpot"}
    inp["CELL"] = {"cellfile": f"{name}.vasp"}
    inp["GRID"] = {"spacing": 0.3}
    inp["EXC"] = {"xc": "LDA"}
    inp["KEDF"] = {"kedf": kedf}
    with open(f"scr-of/{name}.ini", "w") as ofile:
        dump(inp, ofile)

    # run the calculation
    system(f"cd scr-of ; python -m dftpy {name}.ini > {name}.log")

    # grab the energy
    with open(f"scr-of/{name}.log") as ifile:
        for line in ifile:
            if "total energy (a.u.)" in line:
                # divide to get the per unit cell contribution
                return float(line.split()[-1]) / (pts[0] * pts[1] * pts[2])

DFTpy can also be used purely from python but this use of it as an executable matches our BigDFT workflow well. Now let's run the calculations and compare.

results_of = {"TFvW": {}}
for i, a in enumerate(avals):
    results_of["TFvW"][a] = calculate_of(a, [6, 4, 4], f"of-{i}", "TFvW")
plot_all({"BigDFT": results, 
          "TFvW": results_of["TFvW"]})

comparison of bigdft, tfvw

We might wonder if a different kinetic energy functional would match closer to BigDFT. In particular, we can try a non-local functional.

results_of["MGPA"] = {}
for i, a in enumerate(avals):
    results_of["MGPA"][a] = calculate_of(a, [6, 4, 4], f"of-{i}", "MGPA")
plot_all({"BigDFT": results, 
          "TFvW": results_of["TFvW"],
          "MGPA": results_of["MGPA"]})

comparison of bigdft, tfvw, mgpa

This functional indeed matches closer to our BigDFT results - though who is ultimately correct is a different matter. OF-DFT has the benefit of being much cheaper to calculate (it is linear scaling with a low prefactor). With this in mind, it could be interesting to use it to study size dependent effects of systems, with BigDFT's linear scaling mode as a means of checking the result.