Calculate the contributes of electrons for finite temperature energy and entropy
from DOSCAR of VASP

Developed by Tianping Yin based on EF-T-metal.py,
modified by T. Kamiya in 2019

Download: ElectronHelmholtzEnergy/Electron_Helmholtz_energy.py ElectronHelmholtzEnergy/DOSCAR
Run: python Electron_Helmholtz_energy.py

Electron_Helmholtz_energy.py

"""
Calculate the contributes of electrons for finite temperature energy and entropy
from DOSCAR of VASP

Developed by Tianping Yin based on EF-T-metal.py,
modified by T. Kamiya in 2019
"""


import os
import sys
import numpy as np
from numpy import arange
from scipy import integrate
from scipy import optimize
from scipy.interpolate import interp1d
from matplotlib import pyplot as plt

# constants
e = 1.60218e-19 # C
kB = 1.380658e-23 # JK-1


Debug = 0


# Relative accuracy of the quad functin
eps = 1.0e-5
nmaxdiv = 150

# Temperature range
Tmin = 0 # K
Tmax = 1500
Tstep = 50
nT = int((Tmax - Tmin) / Tstep + 1)

#set EF in OUTCAR of VASP
EF0 = 7.5883 # eV


#read from DOSCAR
file = "DOSCAR"
E_raw, dos_raw = np.genfromtxt(file, skip_header=6, usecols=(0,1), unpack=True)
nDOS = len(E_raw)
Emin = E_raw[0]
Emax = E_raw[nDOS-1]
Estep = E_raw[1] - E_raw[0]


# define the DOS function
def f_dos(E):
    global E_raw, dos_raw
    global Emin, Emax, Estep, nDOS

    if E < Emin or Emax < E:
        print("Error in f_dos: Given E={} exceeds the DOS E range [{}, {}]".format(E, Emin, Emax))
        exit()

    fdos = interp1d(E_raw, dos_raw, kind = 'cubic')
    return fdos(E)

# Define the Fermi-Dirac distribution functions
def fe(E, T, EF):
    global e, kB

    if T == 0.0:
        if E < EF:
            return 1.0
        else:
            return 0.0
    return 1.0 / (np.exp((E - EF) * e / kB / T) + 1.0)

def fh(E, T, EF):
    return 1.0 - fe(E, T, EF)

# Define the function to be integrated
def dosfe(E, T, EF):
    return f_dos(E) * fe(E, T, EF)

# Calculate the number of electrons from E0 to E1 with the Fermi level EF at T
def Ne(T, EF, E0, E1):
    if T == 0.0:
        Ne_temp2 = integrate.quad(lambda E: f_dos(E), E0, EF, limit = nmaxdiv, epsrel = eps)
    else:
        Ne_temp2 = integrate.quad(lambda E: dosfe(E, T, EF), E0, E1, limit = nmaxdiv, epsrel = eps)
    if Debug:
        print(" Ne integ error: {}".format(Ne_temp2[1]))
    return Ne_temp2[0]

# Calculate the number of electrons from E0 to E1 (must be EF0) at 0 K,
def Ne0(EF0, E0, E1):
    return Ne(0.0, EF0, E0, E1)


# Calculate EF base on
# the charge neutrality (electron number) condition, Ne(T, EF, dE) - N = 0,
# using the Newton method with the initial value EF0
def CalEF(T, EF0, E0, E1, totNe):
    EF = optimize.newton(lambda EF: Ne(T, EF, E0, E1) - totNe, EF0)
    return EF

# Electron energy
def E_int(T, EF, E0, E1):
    Eint1 = integrate.quad(lambda E: dosfe(E, T, EF) * E, E0, E1, limit = nmaxdiv, epsrel = eps)
    if Debug:
        print(" E_int integ errors: {}".format(Eint1[1]))
    return Eint1[0]

# Mixing term in entropy without the factor of kB
def S(T, EF, E0, E1):
    if T == 0.0:
        return 0.0

    S1 = integrate.quad(lambda E: f_dos(E) * fe(E, T, EF) * np.log(fe(E, T, EF)), E0, E1, limit = nmaxdiv, epsrel = eps)
    S2 = integrate.quad(lambda E: f_dos(E) * fh(E, T, EF) * np.log(fh(E, T, EF)), E0, E1, limit = nmaxdiv, epsrel = eps)
    if Debug:
        print(" S integ errors: {}, {}".format(S1[1], S2[1]))
    return S1[0] + S2[0]

# Entropy * T in eV
def entropy(T, EF, E0, E1):
    return -T * kB / e * S(T, EF, E0, E1)


def main():
    global EF0

    print("quad() parameters")
    print(" releps=", eps)
    print(" nmaxdiv=", nmaxdiv)
    print("")
    print("Temperature range: {} - {}, {} K step".format(Tmin, Tmax, Tstep))
    print("")
    print("DOS configuration")
    print(" Read from [{}]".format(file))
    print(" E range: {} - {}, {} eV step".format(Emin, Emax, Estep))
    print("")
    print("EF at 0K: {} eV".format(EF0))
    Ne0K = Ne0(EF0, Emin, EF0)
    Eint0 = E_int(0.0, EF0, 0.0, EF0)
    print(" total Ne at 0 K: ", Ne0K)
    print(" total E at 0 K: ", Eint0, " eV")
    print("")

    xT = []
    yEF = []
    yEint = []
    yEentropy = []
    yEFa = []
    EFprev = EF0
    print ("%5s\t%8s\t%14s\t%14s\t%14s\t%14s"
            % ("T(K)", "EF(eV)", "dE_ele(eV/u.c.)", "dS_ele*T(eV/u.c.) (approx)", "dF_ele(eV/u.c.)",
               "Ne(0 K) (check)"))
    for i in range(nT):
        T = Tmin + i * Tstep
        kBTe = kB * T / e
        dE = 6 * kBTe

# Integration lowerlimit: Choose one of E0 and compare the result for debugging
# The first one is the most robust for bug, but very slow
# The third one is faster, but use after careful debugging
# E0 = 0.0
# E0 = Emin
        E0 = EFprev - dE
        E1 = EFprev + dE

        if E0 < Emin or Emax < E1:
            print("*** Integration range error: [{}, {}] exceeds DOS E range [{}, {}]".format(E0, E1, Emin, Emax))
            exit()

        Ne0K = Ne0(EF0, E0, EF0)
        Eint0 = E_int(0.0, EF0, E0, EF0)

        EF = CalEF(T, EFprev, E0, E1, Ne0K)
        NeCheck = Ne(T, EF, E0, E1)

        Eint = E_int(T, EF, E0, E1) - Eint0
        E_entropy = entropy(T, EF, E0, E1)
        F_electron = Eint + E_entropy

# The contribution of electrons to dS is roughly in the range [EF - kBT, EF + kBT]
# The maximum contribution to S is at EF with fe = fh = 0.5
        E_entropy_approx = -kBTe * f_dos(EF) * (2.0 * kBTe) * 2.0 * 0.5 * np.log(0.5)

        xT.append(T)
        yEF.append(EF)
        yEint.append(Eint)
        yEentropy.append(E_entropy)
        yEFa.append(F_electron)

        EFprev = EF
        print ("%5.0f\t%12.6g\t%14.6g\t%14.6g(%14.6g)\t%14.6g\t%10.4g(%10.4g)"
                % (T, EF, Eint, E_entropy, E_entropy_approx, F_electron, Ne0K, NeCheck))


#=============================
# Plot graphs
#=============================
    fig = plt.figure(figsize = (10, 10))

    ax1 = fig.add_subplot(2, 2, 1)
    ax2 = fig.add_subplot(2, 2, 2)
    ax3 = fig.add_subplot(2, 2, 4)
    ax1.plot(xT, yEint, label = 'E_int', color = 'navy')
    ax1.plot(xT, yEentropy, label = 'E_entropy', color = 'green')
    ax1.plot(xT, yEFa, label = 'Thermal_electron', color = 'red')
    ax1.set_xlabel("T (K)")
    ax1.set_ylabel("E (eV)")
    ax1.legend()
    ax2.plot(xT, yEF, label = 'EF', color = 'black')
    ax2.set_xlabel("T (K)")
    ax2.set_ylabel("EF (eV)")
    ax2.legend()
    ax3.plot(E_raw, dos_raw, label = 'DOS', color = 'black')
    ax3.plot([EF0, EF0], [min(dos_raw), max(dos_raw)], color = 'red', linestyle = 'dashed')
    ax3.set_xlabel("E (eV)")
    ax3.set_ylabel("DOS (states/u.c.)")
    ax3.legend()
# Rearange the graph axes so that they are not overlapped
    plt.tight_layout()

    plt.pause(0.1)


    print("Press ENTER to exit>>", end = '')
    input()


if __name__ == '__main__':
    main()