Kohn-Sham inversion

from postopus import Run

Thanks to the mapping between the electronic density and the Kohn-Sham potential it is possible to compute the exchange-correlation (xc) potential from the sole knowledge of the density. The usual SCF cycle of a ground-state calculation aims at finding the ground state density by iteratively updating the Hartree and xc potential. However, it is also possible to fix the density and ask the question: what is the xc potential that would produce this density, knowing the external potential acting on the electrons. The procedure that finds this xc potential is known as the Kohn-Sham inversion. This has been extensively used to learn about the exact xc potential for systems in which the exact density is known or where higher theory level can produce high-quality reference densities.

In this tutorial, we will explore how to perform the Kohn-Sham inversion for the simplest case: a one dimensional system with two electrons occupying the same orbital. In this case, the analytical expression of the xc potential is known. For more than two electrons, one needs to use iterative schemes, that we won’t discuss in this tutorial.

Input for the reference density

As usual, we will need to write an input file describing the system we want to calculate. In order to perform the Kohn-Sham inversion, we need first a density. Here, we will use Hartree-Fock theory to evaluate the ground-state of a 1D soft-Coulomb hydrogen atom.

mkdir -p 2-kohn-sham-inversion/density
%%writefile 2-kohn-sham-inversion/density/inp

stdout = 'stdout_gs.txt'
stderr = 'stderr_gs.txt'

CalculationMode = gs
FromScratch = yes

Dimensions = 1

BoxShape = parallelepiped
%Lsize
 15
%
Spacing = 0.05

%Species
'He1D' | species_soft_coulomb | softening | 1 | valence | 2
%

%Coordinates
'He1D' | 0.0
%

TheoryLevel = hartree_fock
AdaptivelyCompressedExchange = yes
ExperimentalFeatures = yes

%Output
 density | axis_x
%

ConvRelDens = 1e-8
EigensolverMaxIter = 50
ExtraStates = 2
ExtraStatesToConverge = 1
Writing 2-kohn-sham-inversion/density/inp

Running this input file will produce the file static/density.y=0,z=0. This is the density we will use in order to perform the Kohn-Sham inversion and find the local xc potential that produces it.

!cd 2-kohn-sham-inversion/density && octopus
run_dens = Run("2-kohn-sham-inversion/density/")
density = run_dens.scf.density()
density.plot()
[<matplotlib.lines.Line2D at 0x7f62793ae5d0>]
../_images/0a0de5b63ad506a58b06b57d7b7bb7eef7021ba8352bf9afea860a8f792f5f7b.png

The eigenvalues are:

run_dens.scf.info.get_eigenvalues()
Spin Eigenvalue Occupation
st
1 -- -0.750249 2.0
2 -- 0.051642 0.0
3 -- 0.055209 0.0

Kohn-Sham inversion

The Kohn-Shame inversion is done using the following input file.

mkdir -p 2-kohn-sham-inversion/inversion
!cp -r 2-kohn-sham-inversion/density/static/density.y=0,z=0 2-kohn-sham-inversion/inversion/density-inp.y=0,z=0
%%writefile 2-kohn-sham-inversion/inversion/inp

stdout = 'stdout_inv.txt'
stderr = 'stderr_inv.txt'

CalculationMode = invert_ks
FromScratch = yes

ExperimentalFeatures = yes

Dimensions = 1

BoxShape = parallelepiped
%Lsize
 15
%
Spacing = 0.05

%Species
'He1D' | species_soft_coulomb | softening | 1 | valence | 2
%

%Coordinates
'He1D' | 0.0
%

XCFunctional = ks_inversion

InvertKSTargetDensity = "density-inp.y=0,z=0"
InvertKSmethod = two_particle

%Output
 potential | axis_x
 density | axis_x
%
Writing 2-kohn-sham-inversion/inversion/inp

We first note that here we change the CalculationMode to be invert_ks instead of gs. Other changes are mandatory. We need to set XCFunctional to ks_inversion. Then, we need to instruct the code where to find the target density. This is specified thanks to the variable InvertKSTargetDensity. Importantly, whereas here we produced the target density using Octopus, with the same grid, there is no requirement on the grid in which the data is provided. If the grid is different, Octopus will perform internally an interpolation to obtain the density on the desired grid.

Finally, we need to choose the methodology used for the Kohn-Sham inversion. As in the present case we are considering the two-electron in one orbital case, we have set InvertKSmethod to two_particle.

!cd 2-kohn-sham-inversion/inversion && octopus
from postopus import Run
import matplotlib.pyplot as plt

run = Run("2-kohn-sham-inversion/inversion")
run.scf.vxc().plot()
plt.title("Potential");
../_images/b679e5fb8177cc0adcefb054366ee522674155ee972f515bf641204f5008bd72.png

Comparison with the slater potential

In the first step of this tutorial, we performed a Hartree-Fock calculation in order to produce the target density. This is a nonlocal theory and in general the only way to obtain the corresponding local potential would be via the solution of the OEP equation. This can be done by Octopus. However, in our case, as we have a single orbital occupied, we know the analytical solution. This is the Slater potential.

We can therefore perform a Koh-Sham DFT calculation for our system using the following the input file.

mkdir -p 2-kohn-sham-inversion/slater
%%writefile 2-kohn-sham-inversion/slater/inp

stdout = 'stdout_gs.txt'
stderr = 'stderr_gs.txt'

CalculationMode = gs
FromScratch = yes

Dimensions = 1

BoxShape = parallelepiped
%Lsize
 15
%
Spacing = 0.05

%Species
'He1D' | species_soft_coulomb | softening | 1 | valence | 2
%

%Coordinates
'He1D' | 0.0   
%

XCFunctional = slater_x

%Output
 potential | axis_x
 density | axis_x
%

ConvRelDens = 1e-8
EigensolverMaxIter = 50
ExtraStates = 2
ExtraStatesToConverge = 1
Writing 2-kohn-sham-inversion/slater/inp
!cd 2-kohn-sham-inversion/slater && octopus

This calculation produces the same eigenvalue and density as the original HF calculation. We can finally compare the Slater potential and the one obtained from the Kohn-Sham inversion. There are few points to note:

  • The Kohn-Sham inversion potential is shifted upwards

  • It goes exactly at zero close to the box edges

run_slater = Run("2-kohn-sham-inversion/slater")

fix, ax = plt.subplots()
run.scf.vxc()[-1].plot(ax=ax, label='Inversion')
run_slater.scf.vxc()[-1].plot(ax=ax, label='Slater')
plt.title("Potential")
plt.legend();
../_images/c0295ce7a4368a2cabf3e10599a970648855f93dc057e565ae5b68a8fd4fb2b8.png

These two points are related to the treatment of the asymptotic part of the potential and are limitations of the current implementation. To prevent numerical noise, the current implementation sets to zero the points where the density is too samll. As the xc potential can be defined up to a constant, this freedom is used to avoid discontinuity of the potential where we set it to zero.

Note, that the eigenvalue of the occupied state is reproduced exactly. This is not the case for the unoccupied states.

run_slater.scf.info.get_eigenvalues()
Spin Eigenvalue Occupation
st
1 -- -0.750249 2.0
2 -- -0.256559 0.0
3 -- -0.142115 0.0

By shifting the result of the Kohn-Sham inversion, we find that this matches perfectly the Slater potential. However, one can employ the fact that the interaction is a soft-Coulomb potential. This allows for an analytical treatment of the asymptotic region. This is done using the variable KSInversionAsymptotics to xc_asymptotics_sc. If you rerun the Kohn-Sham inversion with this method, you should now properly reproduce perfectly the Slater potential.

Tutorial Validation Checks

from postopus import Run
import numpy as np
from tutorial_helpers.extract_peak import extract_peak

Reference density

density = run.scf.density()
last_dens = density[-1]
np.testing.assert_allclose(extract_peak(last_dens['x'].to_numpy(), last_dens.to_numpy(), order=1), (0.0, 1.8931844073188755, 0.931127246983921), rtol=0.001)

Inversion potential

pot = run.scf.vxc()
np.testing.assert_allclose(extract_peak(pot['x'].to_numpy(), -pot[-1].to_numpy(), order=1), (0.0, 4.559799503442996, 0.734672997816137), rtol=0.001)

Slater

run_slater = Run("2-kohn-sham-inversion/slater/")
np.testing.assert_allclose(run_slater.scf.info.get_total_energy(),-2.22420955, rtol=0.001)
np.testing.assert_allclose(run_slater.scf.info.get_eigenvalues()['Eigenvalue'].values[0],
                           run_dens.scf.info.get_eigenvalues()['Eigenvalue'].values[0], rtol=0.001)