811 lines
38 KiB
Python
811 lines
38 KiB
Python
import numpy as np
|
|
import os
|
|
import time
|
|
import math
|
|
import matplotlib.pyplot as plt
|
|
from scipy.interpolate import griddata
|
|
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
from scipy.ndimage.filters import gaussian_filter1d
|
|
import tip_types
|
|
from echem.core import ElemNum2Name
|
|
plt.switch_backend('agg')
|
|
|
|
|
|
class STM:
|
|
"""
|
|
This class is for computing 2D and 3D STM images with different theory levels:
|
|
Tersoff-Hummann, Chen, analytical acceptor wavefucntions (oxygen)
|
|
Also this class can calculate 2D ECSTM images using GerisherMarcus module
|
|
|
|
In current version of program this class is no more needed. Most of functions are relocated to kHET_spatial.py
|
|
"""
|
|
|
|
PLANCK_CONSTANT = 4.135667662e-15 # Planck's constant in eV*s
|
|
BOLTZMANN_CONSTANT = 8.617333262145e-5 # Boltzmann's constant in eV/K
|
|
ELEM_CHARGE = 1.60217662e-19 # Elementary charge in Coulombs
|
|
BOHR_RADIUS = 1.88973
|
|
AVAILABLE_TIPS_TYPES = ['oxygen', 'IrCl6', 'RuNH3_6', 'RuNH3_6_NNN_plane', 'RuNH3_6_perpendicular', 'oxygen_parallel_x', 'oxygen_parallel_y']
|
|
|
|
def __init__(self, working_folder):
|
|
self.WF_ijke = None
|
|
self.CD_ijke = None
|
|
self.dE = None
|
|
self.energy_range = None
|
|
self.working_folder = working_folder
|
|
self.path_to_data = self.working_folder+'/Saved_data'
|
|
self.WF_data_path = self.path_to_data+'/WF_data.npy'
|
|
self.WF_ijke_path = self.path_to_data+'/WF_ijke.npy'
|
|
self.CD_ijke_path = self.path_to_data+'/CD_ijke.npy'
|
|
self.poscar_path = working_folder+'/POSCAR'
|
|
|
|
def save_as_vasp(self, array, name, dir):
|
|
if not os.path.exists(dir):
|
|
print(f"Directory {dir} does not exist. Creating directory")
|
|
os.mkdir(dir)
|
|
shape = np.shape(array)
|
|
with open(self.poscar_path) as inf:
|
|
lines = inf.readlines()
|
|
natoms = int(lines[6].strip())
|
|
with open(dir+'/'+ name + '.vasp', 'w') as ouf:
|
|
ouf.writelines(lines[:natoms+8])
|
|
ouf.write('\n ' + str(shape[0]) + ' ' + str(shape[1]) + ' ' + str(shape[2]) + '\n ')
|
|
counter = 0
|
|
for k in range(shape[2]):
|
|
for j in range(shape[1]):
|
|
for i in range(shape[0]):
|
|
ouf.write(str('%.8E' % array[i][j][k]) + ' ')
|
|
counter += 1
|
|
if counter % 10 == 0:
|
|
ouf.write('\n ')
|
|
print(f"File {name} saved")
|
|
|
|
def save_as_cube(self, array, name, dir):
|
|
if not os.path.exists(dir):
|
|
print(f"Directory {dir} does not exist. Creating directory")
|
|
os.mkdir(dir)
|
|
|
|
shape = np.shape(array)
|
|
with open(self.poscar_path) as inf:
|
|
lines = inf.readlines()
|
|
natoms = sum(map(int, lines[6].strip().split()))
|
|
atomtypes = lines[5].strip().split()
|
|
numbers_of_atoms = list(map(int, lines[6].strip().split()))
|
|
type_of_i_atom = []
|
|
basis = []
|
|
for i in [2,3,4]:
|
|
vector = list(map(float, lines[i].strip().split()))
|
|
basis.append(vector)
|
|
basis = np.array(basis)
|
|
for i, number in enumerate(numbers_of_atoms):
|
|
for j in range(number):
|
|
type_of_i_atom.append(atomtypes[i])
|
|
|
|
with open(dir+'/'+ name + '.cube', 'w') as ouf:
|
|
ouf.write(' This file is generated using stm.py module\n')
|
|
ouf.write(' Good luck\n')
|
|
ouf.write(' ' + str(natoms) + '\t0.000\t0.000\t0.000\n')
|
|
ouf.write(' ' + str(-shape[0]) + lines[2])
|
|
ouf.write(' ' + str(-shape[1]) + lines[3])
|
|
ouf.write(' ' + str(-shape[2]) + lines[4])
|
|
for i, line in enumerate(lines[8:natoms+8]):
|
|
coordinate = np.array(list(map(float, line.strip().split())))
|
|
if lines[7].strip() == 'Direct':
|
|
coordinate = coordinate.dot(basis)
|
|
coordinate *= self.BOHR_RADIUS
|
|
elif lines[7].strip() == 'Cartesian':
|
|
coordinate *= self.BOHR_RADIUS
|
|
else:
|
|
print ('WARNING!!! Cannot read POSCAR correctly')
|
|
atomtype = type_of_i_atom[i]
|
|
atomnumber = list(ElemNum2Name.keys())[list(ElemNum2Name.values()).index(atomtype)]
|
|
ouf.write(' ' + str(atomnumber) + '\t0.00000\t' + str(coordinate[0])+ '\t' + str(coordinate[1])+ '\t' + str(coordinate[2])+'\n')
|
|
counter = 0
|
|
for i in range(shape[0]):
|
|
for j in range(shape[1]):
|
|
for k in range(shape[2]):
|
|
ouf.write(str('%.5E' % array[i][j][k]) + ' ')
|
|
counter += 1
|
|
if counter % 6 == 0:
|
|
ouf.write('\n ')
|
|
ouf.write('\n ')
|
|
print(f"File {name} saved")
|
|
|
|
|
|
def load_data(self):
|
|
WF_data = np.load(self.WF_data_path, allow_pickle=True).item()
|
|
self.dE = WF_data['dE']
|
|
self.energy_range = WF_data['energy_range']
|
|
self.WF_ijke = np.load(self.WF_ijke_path)
|
|
self.CD_ijke = np.load(self.CD_ijke_path)
|
|
|
|
def set_ecstm_parameters(self, C_EDL, T, lambda_, V_std, overpot,
|
|
effective_freq, linear_constant, threshold_value):
|
|
"""
|
|
:param C_EDL: float
|
|
Capacitance of electric double layer (microF/cm^2)
|
|
:param T: int, float
|
|
Temperature. It is used in computing Fermi function and distribution function of redox system states
|
|
:param lambda_: float
|
|
Reorganization energy in eV
|
|
:param V_std: float
|
|
Standart potential of the redox couple (Volts)
|
|
:param overpot: float
|
|
Overpotential (Volts). It shifts the electrode Fermi energy to -|e|*overpot
|
|
:param effective_freq: float
|
|
Effective frequency of redox species motion
|
|
:param effective_freq: float
|
|
Linear constant of proportionality of Hif and Sif: Hif = linear_constant * Sif
|
|
:param threshold_value: float
|
|
Minimum value of y_redox and y_fermi to be considered in integral
|
|
:return:
|
|
"""
|
|
self.C_EDL = C_EDL
|
|
self.T = T
|
|
self.lambda_ = lambda_
|
|
self.sheet_area = self._get_sheet_area() # Area of investigated surface(XY) in cm^2
|
|
self.V_std = V_std
|
|
self.overpot = overpot
|
|
self.effective_freq = effective_freq
|
|
self.linear_constant = linear_constant
|
|
self.threshold_value = threshold_value
|
|
|
|
def load_data_for_ecstm(self):
|
|
"""
|
|
This inner function load necessary data for ECSTM calculations
|
|
:param outcar_path: str
|
|
path to OUTCAR vasp file
|
|
:param locpot_path: str
|
|
path to LOCPOT vasp file
|
|
:return:
|
|
"""
|
|
try:
|
|
self.E = np.load(self.path_to_data+'/E.npy')
|
|
self.DOS = np.load(self.path_to_data+'/DOS.npy')
|
|
except:
|
|
import preprocessing_v2 as preprocessing
|
|
p = preprocessing.Preprocessing(working_folder=self.working_folder)
|
|
p.process_OUTCAR()
|
|
self.E, self.DOS, dE_new = p.get_DOS(self.energy_range, self.dE)
|
|
if dE_new != self.dE:
|
|
print("WARNING! Something wrong with dE during DOS calculations")
|
|
np.save(self.path_to_data+'/DOS.npy', self.DOS)
|
|
np.save(self.path_to_data+'/E.npy', self.E)
|
|
try:
|
|
self.efermi = np.load(self.path_to_data+'/efermi.npy')
|
|
except:
|
|
print(f"ERROR! {self.path_to_data}/efermi.npy does not exist. Try to preprocess data")
|
|
try:
|
|
self.vacuum_lvl = np.load(self.path_to_data+'vacuum_lvl.npy')
|
|
except:
|
|
from pymatgen.io.vasp.outputs import Locpot
|
|
locpot = Locpot.from_file(self.working_folder+'/LOCPOT')
|
|
avr = locpot.get_average_along_axis(2)
|
|
self.vacuum_lvl = np.max(avr)
|
|
np.save(self.path_to_data+'/vacuum_lvl.npy', self.vacuum_lvl)
|
|
|
|
def _calculate_distributions(self):
|
|
"""
|
|
This function calls GerisherMarcus module with GM class to calculate distribution of redox species
|
|
and Fermi-Dirac distribution according to Gerisher-Marcformalismizm
|
|
:return:
|
|
"""
|
|
import GerischerMarkus as gm
|
|
gerisher_marcus_obj = gm.GM(path_to_data = self.path_to_data)
|
|
gerisher_marcus_obj.set_params(self.C_EDL, self.T, self.lambda_, self.sheet_area)
|
|
self.y_fermi, self.y_redox = gerisher_marcus_obj.compute_distributions(self.V_std, overpot=self.overpot)
|
|
return gerisher_marcus_obj
|
|
|
|
@staticmethod
|
|
def _nearest_array_indices(array, value):
|
|
i = 0
|
|
while value > array[i]:
|
|
i += 1
|
|
return i - 1, i
|
|
|
|
def plot_distributions(self, E_range=[-7, 4], dE=None, sigma=2, fill_area_lower_Fermi_lvl=True,
|
|
plot_Fermi_Dirac_distib=True, plot_redox_distrib=True):
|
|
a = self._calculate_distributions()
|
|
if dE == None:
|
|
dE = self.dE
|
|
if dE != self.dE or E_range[0] < self.energy_range[0] or E_range[1] > self.energy_range[1]:
|
|
import preprocessing_v2 as prep
|
|
p = prep.Preprocessing(working_folder = self.working_folder)
|
|
E, DOS, dE_new = p.get_DOS(E_range, dE)
|
|
else:
|
|
E, DOS = a.E, a.DOS
|
|
n_1, n_2 = self._nearest_array_indices(E, a.dE_Q_eq)
|
|
if sigma > 0 and sigma != None:
|
|
DOS = gaussian_filter1d(DOS, sigma)
|
|
plt.plot(E, DOS)
|
|
if fill_area_lower_Fermi_lvl == True:
|
|
plt.fill_between(E[:n_2], DOS[:n_2])
|
|
if plot_Fermi_Dirac_distib == True:
|
|
plt.plot(a.E, self.y_fermi * 30)
|
|
if plot_redox_distrib == True:
|
|
plt.plot(a.E, self.y_redox * 10)
|
|
plt.xlabel('E, eV')
|
|
plt.ylabel('DOS, states/eV/cell')
|
|
plt.xlim(E_range)
|
|
plt.savefig(f"distributions_{self.V_std}_{self.overpot}.png", dpi=300)
|
|
plt.close()
|
|
|
|
def _kT_in_eV(self, T):
|
|
return T * self.BOLTZMANN_CONSTANT
|
|
|
|
def _get_kappa(self, matrix_elements_squared):
|
|
lz_factor = 2 * matrix_elements_squared / self.PLANCK_CONSTANT / self.effective_freq * math.sqrt(
|
|
math.pi / self.lambda_ / self._kT_in_eV(self.T))
|
|
kappa = 1 - np.exp(-2 * math.pi * lz_factor)
|
|
return kappa
|
|
|
|
def _get_basis_vectors(self):
|
|
"""
|
|
This function processes POSCAR file to extract vectors of the simulation box
|
|
:param poscar_path:
|
|
:return:
|
|
"""
|
|
with open(self.poscar_path) as inf:
|
|
lines = inf.readlines()
|
|
b1 = np.array(list(map(float, lines[2].strip().split())))
|
|
b2 = np.array(list(map(float, lines[3].strip().split())))
|
|
b3 = np.array(list(map(float, lines[4].strip().split())))
|
|
return b1, b2, b3
|
|
|
|
def _get_sheet_area(self):
|
|
"""
|
|
Inner function to calculate sheet_area (XY plane) in cm^2
|
|
"""
|
|
b1, b2, b3 = self._get_basis_vectors()
|
|
return np.linalg.norm(np.cross(b1,b2))*1e-16
|
|
|
|
def _plot_contour_map(self, function, X, Y, dir, filename):
|
|
"""
|
|
Function for plotting 2D images
|
|
:param function: Z values
|
|
:param X: X values
|
|
:param Y: Y values
|
|
:param dir: directory in which contour map will be saved
|
|
:param filename: filename of image
|
|
:return:
|
|
"""
|
|
if not os.path.exists(dir):
|
|
print(f"Directory {dir} does not exist. Creating directory")
|
|
os.mkdir(dir)
|
|
|
|
t = time.time()
|
|
Z = function.transpose().flatten()
|
|
xi = np.linspace(X.min(), X.max(), 1000)
|
|
yi = np.linspace(Y.min(), Y.max(), 1000)
|
|
zi = griddata((X, Y), Z, (xi[None, :], yi[:, None]), method='cubic')
|
|
plt.contourf(xi, yi, zi, 500, cmap=plt.cm.rainbow)
|
|
ax = plt.gca()
|
|
ax.set_aspect('equal')
|
|
divider = make_axes_locatable(ax)
|
|
cax = divider.append_axes("right", size="7%", pad=0.1)
|
|
plt.colorbar(cax=cax)
|
|
plt.savefig(dir+'/'+filename+'.png', dpi=300)
|
|
plt.close()
|
|
print (f"Plotting map takes {time.time()-t} sec")
|
|
|
|
|
|
def _get_overlap_integrals_squared(self, e, cutoff, acc_orbitals, zmin, zmax):
|
|
WF_ijk = self.WF_ijke[:, :, :, e]
|
|
xlen, ylen, zlen = np.shape(WF_ijk)
|
|
overlap_integrals_squared = np.zeros((xlen, ylen))
|
|
if np.allclose(WF_ijk, np.zeros((xlen, ylen, zlen))):
|
|
# If WF_ijk array for current energy is empty, code goes to the next energy
|
|
print(f"WF_ijk array for energy {e} is empty. Going to the next")
|
|
return overlap_integrals_squared
|
|
for i in range(xlen):
|
|
if i - cutoff < 0:
|
|
WF_ijk_rolled_x = np.roll(WF_ijk, cutoff, axis=0)
|
|
orb_rolled_x = []
|
|
for orbital in acc_orbitals:
|
|
orb_rolled_x.append(np.roll(orbital, i + cutoff, axis=0))
|
|
xmin = i
|
|
xmax = i + cutoff * 2
|
|
elif i - cutoff >= 0 and i + cutoff <= xlen:
|
|
WF_ijk_rolled_x = np.copy(WF_ijk)
|
|
orb_rolled_x = []
|
|
for orbital in acc_orbitals:
|
|
orb_rolled_x.append(np.roll(orbital, i, axis = 0))
|
|
xmin = i - cutoff
|
|
xmax = i + cutoff
|
|
elif i + cutoff > xlen:
|
|
WF_ijk_rolled_x = np.roll(WF_ijk, -cutoff, axis=0)
|
|
orb_rolled_x = []
|
|
for orbital in acc_orbitals:
|
|
orb_rolled_x.append(np.roll(orbital, i - cutoff, axis=0))
|
|
xmin = i - cutoff * 2
|
|
xmax = i
|
|
else:
|
|
print(f"ERROR: for i = {i} something with rolling arrays along x goes wrong")
|
|
for j in range(ylen):
|
|
if j - cutoff < 0:
|
|
WF_ijk_rolled = np.roll(WF_ijk_rolled_x, cutoff, axis=1)
|
|
orb_rolled = []
|
|
for orbital in orb_rolled_x:
|
|
orb_rolled.append(np.roll(orbital, j + cutoff, axis=1))
|
|
ymin = j
|
|
ymax = j + cutoff * 2
|
|
elif j - cutoff >= 0 and j + cutoff <= ylen:
|
|
WF_ijk_rolled = np.copy(WF_ijk_rolled_x)
|
|
orb_rolled = []
|
|
for orbital in orb_rolled_x:
|
|
orb_rolled.append(np.roll(orbital, j, axis=1))
|
|
ymin = j - cutoff
|
|
ymax = j + cutoff
|
|
elif j + cutoff > ylen:
|
|
WF_ijk_rolled = np.roll(WF_ijk_rolled_x, -cutoff, axis=1)
|
|
orb_rolled = []
|
|
for orbital in orb_rolled_x:
|
|
orb_rolled.append(np.roll(orbital, j - cutoff, axis=1))
|
|
ymin = j - cutoff * 2
|
|
ymax = j
|
|
else:
|
|
print(f"ERROR: for i = {i} something with rolling arrays along y goes wrong")
|
|
|
|
integral = []
|
|
for orbital in orb_rolled:
|
|
integral.append(np.linalg.norm(WF_ijk_rolled[xmin:xmax, ymin:ymax, zmin:zmax]*\
|
|
orbital[xmin:xmax, ymin:ymax, zmin:zmax]))
|
|
overlap_integrals_squared[i][j] = max(integral) ** 2
|
|
|
|
return overlap_integrals_squared
|
|
|
|
def generate_acceptor_orbitals(self, orb_type, shape, z_shift=0, x_shift=0, y_shift=0):
|
|
"""
|
|
This is generator of acceptor orbitals using tip_types.py module
|
|
:return:
|
|
"""
|
|
bohr_radius = 0.529 #TODO better to get it from core.constants
|
|
b1, b2, b3 = self._get_basis_vectors()
|
|
bn1 = b1 / shape[0]
|
|
bn2 = b2 / shape[1]
|
|
bn3 = b3 / shape[2]
|
|
basis = np.array([bn1, bn2, bn3])
|
|
transition_matrix = basis.transpose()
|
|
numer_of_orbitals = len(tip_types.orbitals(orb_type))
|
|
acc_orbitals = []
|
|
for i in range(numer_of_orbitals):
|
|
acc_orbitals.append(np.zeros(shape))
|
|
for i in range(shape[0]):
|
|
for j in range(shape[1]):
|
|
for k in range(shape[2]):
|
|
if i - x_shift >= shape[0] / 2:
|
|
x = i - shape[0] - x_shift
|
|
else:
|
|
x = i - x_shift
|
|
if j - y_shift >= shape[1] / 2:
|
|
y = j - shape[1] - y_shift
|
|
else:
|
|
y = j - y_shift
|
|
if k - z_shift >= shape[2] / 2:
|
|
z = k - shape[2] - z_shift
|
|
else:
|
|
z = k - z_shift
|
|
r = np.dot(transition_matrix, np.array([x, y, z])) / bohr_radius
|
|
for o, orbital in enumerate(acc_orbitals):
|
|
orbital[i][j][k] = tip_types.orbitals(orb_type)[o](r)
|
|
return acc_orbitals
|
|
|
|
|
|
def calculate_2D_ECSTM(self, z_position, tip_type='oxygen', dir='ECSTM', cutoff_in_Angstroms=5, all_z=False, from_CD=False):
|
|
"""
|
|
This function calculate 2D ECSTM images using GerisherMarcus module
|
|
:param z_position: position of tip under the investigated surface
|
|
:param tip_type: Type of tip orbital. Currenty only "oxygen" is available
|
|
:param dir: directiry to save images
|
|
:param cutoff_in_Angstroms: Cutoff over x,y and z, when overlap integral is calculated.
|
|
E.g. for x direction area of integration will be from x-cutoff to x+cutoff
|
|
:return:
|
|
"""
|
|
|
|
print(f"Starting to calculate 2D ECSTM; tip_type = {tip_type};")
|
|
xlen, ylen, zlen, elen = np.shape(self.WF_ijke)
|
|
b1, b2, b3 = self._get_basis_vectors()
|
|
bn1 = b1 / xlen
|
|
bn2 = b2 / ylen
|
|
bn3 = b3 / zlen
|
|
|
|
print (bn1, bn2, xlen, ylen)
|
|
|
|
if b3[0] != 0.0 or b3[1] != 0.0:
|
|
print("WARNING! You z_vector is not perpendicular to XY plane, Check calculate_2D_STM function")
|
|
|
|
z = int(zlen // 2 + z_position // np.linalg.norm(bn3))
|
|
real_z_position = z_position // np.linalg.norm(bn3) * np.linalg.norm(bn3)
|
|
print(f"Real z_position of tip = {real_z_position}")
|
|
|
|
R = []
|
|
for j in range(ylen):
|
|
for i in range(xlen):
|
|
R.append(i * bn1 + j * bn2)
|
|
X = np.array([x[0] for x in R])
|
|
Y = np.array([x[1] for x in R])
|
|
|
|
if any(tip_type == kw for kw in self.AVAILABLE_TIPS_TYPES):
|
|
cutoff = int(cutoff_in_Angstroms // np.linalg.norm(bn1))
|
|
if cutoff > xlen // 2 or cutoff > ylen // 2:
|
|
print("ERROR: Cutoff should be less than 1/2 of each dimension of the cell. "
|
|
"Try to reduce cutoff. Otherwise, result could be unpredictible")
|
|
print(f"Cutoff in int = {cutoff}")
|
|
ECSTM_ij = np.zeros((xlen, ylen))
|
|
acc_orbitals = self.generate_acceptor_orbitals(tip_type, (xlen, ylen, zlen), z_shift=z)
|
|
if all_z == True:
|
|
zmin = 0
|
|
zmax = zlen
|
|
elif z - cutoff >= 0 and z + cutoff <= zlen:
|
|
zmin = z - cutoff
|
|
zmax = z + cutoff
|
|
else:
|
|
print("Can't reduce integrating area in z dimention. "
|
|
"Will calculate overlapping for all z. You can ignore this message "
|
|
"if you don't care about efficiency")
|
|
zmin = 0
|
|
zmax = zlen
|
|
self._calculate_distributions()
|
|
for e in range(elen):
|
|
if self.y_redox[e] < self.threshold_value or self.y_fermi[e] < self.threshold_value:
|
|
continue
|
|
overlap_integrals_squared = self._get_overlap_integrals_squared(e, cutoff, acc_orbitals, zmin, zmax)
|
|
matrix_elements_squared = overlap_integrals_squared * self.linear_constant * np.linalg.norm(bn3)**3
|
|
#print ('Hif = ', matrix_elements_squared, 'eV')
|
|
#kappa = self._get_kappa(matrix_elements_squared)
|
|
#ECSTM_ij += kappa * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE * 2 * math.pi *\
|
|
# self.ELEM_CHARGE / self.PLANCK_CONSTANT
|
|
ECSTM_ij += 2 * np.pi / self.PLANCK_CONSTANT * matrix_elements_squared * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE
|
|
|
|
|
|
|
|
elif tip_type == 's':
|
|
ECSTM_ij = np.zeros((xlen, ylen))
|
|
self._calculate_distributions()
|
|
for e in range(elen):
|
|
if self.y_redox[e] < self.threshold_value or self.y_fermi[e] < self.threshold_value:
|
|
continue
|
|
if from_CD == True:
|
|
matrix_elements_squared = self.CD_ijke[:, :, z, e]
|
|
ECSTM_ij += 2 * np.pi / self.PLANCK_CONSTANT * matrix_elements_squared * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE
|
|
else:
|
|
matrix_elements_squared = np.abs(self.WF_ijke[:, :, z, e]) ** 2
|
|
#print ('Hif = ', matrix_elements_squared, 'eV')
|
|
#kappa = self._get_kappa(matrix_elements_squared)
|
|
#ECSTM_ij += kappa * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE * 2 * math.pi *\
|
|
# self.ELEM_CHARGE / self.PLANCK_CONSTANT\
|
|
#TODO - check formula!!!
|
|
ECSTM_ij += 2 * np.pi / self.PLANCK_CONSTANT * matrix_elements_squared * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE
|
|
|
|
|
|
elif tip_type == 'pz':
|
|
ECSTM_ij = np.zeros((xlen, ylen))
|
|
self._calculate_distributions()
|
|
for e in range(elen):
|
|
if self.y_redox[e] < self.threshold_value or self.y_fermi[e] < self.threshold_value:
|
|
continue
|
|
grad_z_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=2)
|
|
matrix_elements_squared = (np.abs(grad_z_WF_for_e[:, :, z])) ** 2
|
|
print ('Hif = ', matrix_elements_squared, 'eV')
|
|
#kappa = self._get_kappa(matrix_elements_squared)
|
|
#ECSTM_ij += kappa * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE * 2 * math.pi *\
|
|
# self.ELEM_CHARGE / self.PLANCK_CONSTANT
|
|
ECSTM_ij += 2 * np.pi / self.PLANCK_CONSTANT * matrix_elements_squared * self.y_fermi[e] * self.DOS[e] * self.y_redox[e] * self.dE
|
|
|
|
if from_CD:
|
|
cd_flag='from_CD'
|
|
else:
|
|
cd_flag=''
|
|
filename = f"ecstm_{'%.2f'%real_z_position}_{tip_type}_{cd_flag}_{self.V_std}_{self.overpot}_{self.lambda_}"
|
|
self._plot_contour_map(ECSTM_ij, X, Y, dir, filename)
|
|
np.save(dir+'/'+filename, ECSTM_ij)
|
|
|
|
|
|
def calculate_2D_STM(self, STM_energy_range, z_position, tip_type='s', dir='STM_2D', cutoff_in_Angstroms=5, all_z=False, from_CD=False):
|
|
"""
|
|
Function for calculation 2D STM images
|
|
:param STM_energy_range: Energy range regarding Fermi level
|
|
:param z_position: Position of tip under the surface. This distance is in Angstroms assuming
|
|
that investigated surface XY is in the center of calculation cell (the middle of z axes)
|
|
:param tip_type: type of tip orbital
|
|
:param dir: directory for saving images
|
|
:param cutoff_in_Angstroms: Cutoff over x,y and z, when overlap integral is calculated.
|
|
E.g. for x direction area of integration will be from x-cutoff to x+cutoff
|
|
:return:
|
|
"""
|
|
emin = int((STM_energy_range[0] - self.energy_range[0]) / self.dE)
|
|
emax = int((STM_energy_range[1] - self.energy_range[0]) / self.dE)
|
|
print(f"Starting to calculate 2D STM; tip_type = {tip_type}; STM energy range = {STM_energy_range}")
|
|
e_array = np.arange(emin, emax)
|
|
xlen, ylen, zlen, elen = np.shape(self.WF_ijke)
|
|
b1, b2, b3 = self._get_basis_vectors()
|
|
bn1 = b1 / xlen
|
|
bn2 = b2 / ylen
|
|
bn3 = b3 / zlen
|
|
|
|
if b3[0] != 0.0 or b3[1] != 0.0 :
|
|
print("WARNING! You z_vector is not perpendicular to XY plane, Check calculate_2D_STM function")
|
|
|
|
z = int(zlen // 2 + z_position // np.linalg.norm(bn3))
|
|
real_z_position = z_position // np.linalg.norm(bn3) * np.linalg.norm(bn3)
|
|
print(f"Real z_position of tip = {real_z_position}")
|
|
|
|
R = []
|
|
for j in range(ylen):
|
|
for i in range(xlen):
|
|
R.append(i * bn1 + j * bn2)
|
|
X = np.array([x[0] for x in R])
|
|
Y = np.array([x[1] for x in R])
|
|
|
|
if any(tip_type == kw for kw in self.AVAILABLE_TIPS_TYPES):
|
|
cutoff = int(cutoff_in_Angstroms // np.linalg.norm(bn1))
|
|
if cutoff > xlen//2 or cutoff > ylen//2:
|
|
print("ERROR: Cutoff should be less than 1/2 of each dimension of the cell. "
|
|
"Try to reduce cutoff. Otherwise, result could be unpredictible")
|
|
print (f"Cutoff in int = {cutoff}")
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
acc_orbitals = self.generate_acceptor_orbitals(tip_type, (xlen, ylen, zlen), z_shift=z)
|
|
if all_z == True:
|
|
zmin = 0
|
|
zmax = zlen
|
|
elif z - cutoff >=0 and z + cutoff <= zlen:
|
|
zmin = z-cutoff
|
|
zmax = z+cutoff
|
|
else:
|
|
print("Can't reduce integrating area in z dimention. "
|
|
"Will calculate overlapping for all z. You can ignore this message "
|
|
"if you don't care about efficiency")
|
|
zmin = 0
|
|
zmax = zlen
|
|
for e in e_array:
|
|
overlap_integrals_squared = self._get_overlap_integrals_squared(e, cutoff, acc_orbitals, zmin, zmax)
|
|
STM_ij += overlap_integrals_squared
|
|
|
|
elif tip_type == 's':
|
|
if from_CD == True:
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
STM_ij += self.CD_ijke[:, :, z, e]
|
|
else:
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
STM_ij += np.abs(self.WF_ijke[:, :, z, e])**2
|
|
|
|
elif tip_type == 'pz':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_z_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=2)
|
|
STM_ij += (np.abs(grad_z_WF_for_e[:, :, z])) ** 2
|
|
|
|
elif tip_type == 'px':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=0)
|
|
STM_ij += (np.abs(grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p30':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=1)
|
|
STM_ij += (np.abs(grad_y_WF_for_e + grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p60':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=1)
|
|
STM_ij += (np.abs(grad_y_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p90':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=1)
|
|
STM_ij += (np.abs(grad_y_WF_for_e - grad_x_WF_for_e / 2.0)) ** 2
|
|
|
|
elif tip_type == 'p120':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=1)
|
|
STM_ij += (np.abs(grad_y_WF_for_e - grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p150':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, z, e], axis=1)
|
|
STM_ij += (np.abs(grad_y_WF_for_e / 2.0 - grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 's+pz':
|
|
STM_ij = np.zeros((xlen, ylen))
|
|
for e in e_array:
|
|
grad_z_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=2)
|
|
STM_ij += (np.abs(self.WF_ijke[:, :, z, e] + grad_z_WF_for_e[:, :, z])) ** 2
|
|
|
|
if from_CD:
|
|
cd_flag='from_CD'
|
|
else:
|
|
cd_flag=''
|
|
filename = 'stm_' + str('%.2f' % real_z_position) + '_' + tip_type + '_' + cd_flag
|
|
self._plot_contour_map(STM_ij, X, Y, dir, filename)
|
|
np.save(dir+'/'+filename, STM_ij)
|
|
|
|
|
|
def calculate_3D_STM(self, STM_energy_range, tip_type='s', dir='STM_3D', format='cube', from_CD=False):
|
|
emin = int((STM_energy_range[0] - self.energy_range[0]) / self.dE)
|
|
emax = int((STM_energy_range[1] - self.energy_range[0]) / self.dE)
|
|
print ('Starting to calculate 3D STM; tip_type ='+tip_type+'; STM energy range = ', STM_energy_range)
|
|
e_array = np.arange(emin, emax)
|
|
xlen, ylen, zlen, elen = np.shape(self.WF_ijke)
|
|
|
|
if tip_type == 's':
|
|
if from_CD == True:
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
STM_ijk += self.CD_ijke[:, :, :, e]
|
|
else:
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
STM_ijk += np.abs(self.WF_ijke[:, :, :, e]) ** 2
|
|
|
|
elif tip_type == 'pz':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_z_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=2)
|
|
STM_ijk += (np.abs(grad_z_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'px':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=0)
|
|
STM_ijk += (np.abs(grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p30':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=1)
|
|
STM_ijk += (np.abs(grad_y_WF_for_e + grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p60':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=1)
|
|
STM_ijk += (np.abs(grad_y_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p90':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=1)
|
|
STM_ijk += (np.abs(grad_y_WF_for_e - grad_x_WF_for_e / 2.0)) ** 2
|
|
|
|
elif tip_type == 'p120':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=1)
|
|
STM_ijk += (np.abs(grad_y_WF_for_e - grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 'p150':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_x_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=0)
|
|
grad_y_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=1)
|
|
STM_ijk += (np.abs(grad_y_WF_for_e / 2.0 - grad_x_WF_for_e)) ** 2
|
|
|
|
elif tip_type == 's+pz':
|
|
STM_ijk = np.zeros((xlen, ylen, zlen))
|
|
for e in e_array:
|
|
grad_z_WF_for_e = np.gradient(self.WF_ijke[:, :, :, e], axis=2)
|
|
STM_ijk += (np.abs(self.WF_ijke[:, :, :, e] + grad_z_WF_for_e)) ** 2
|
|
|
|
if not os.path.exists(dir):
|
|
print(f"Directory {dir} does not exist. Creating directory")
|
|
os.mkdir(dir)
|
|
|
|
filename = 'stm_'+str(STM_energy_range[0])+'_'+str(STM_energy_range[1])+'_'+tip_type
|
|
if format == 'vasp':
|
|
self.save_as_vasp(STM_ijk, filename, dir)
|
|
elif format == 'cube':
|
|
self.save_as_cube(STM_ijk, filename, dir)
|
|
|
|
|
|
if __name__=="__main__":
|
|
t = time.time()
|
|
stm = STM(path_to_data='Saved_data', poscar_path='../POSCAR')
|
|
stm.load_data()
|
|
|
|
###CALCULATE 2D STM IMAGES###
|
|
#stm.calculate_2D_STM((-0.5, 0.0), 3.5, tip_type='oxygen', cutoff_in_Angstroms=5)
|
|
#stm.calculate_2D_STM((-0.5, 0.0), 3.0, tip_type='s')
|
|
#stm.calculate_2D_STM((-0.5, 0.0), 3.0, tip_type='pz')
|
|
#stm.calculate_2D_STM((-0.5, 0.0), 3.0, tip_type='s+pz')
|
|
#stm.calculate_2D_STM((-0.5, 0.0), 4.5, tip_type='IrCl6', cutoff_in_Angstroms=8)
|
|
#stm.calculate_2D_STM((-0.5, 0.0), 5.5, tip_type='RuNH3_6_NNN_plane', cutoff_in_Angstroms=8)
|
|
|
|
###CALCULATE 2D ECSTM IMAGES###
|
|
stm.load_data_for_ecstm(outcar_path='../OUTCAR', locpot_path='../LOCPOT')
|
|
|
|
#for V_std in [-1.0, -0.5, 0.0, 0.5, 1.0]:
|
|
#for V_std in [-0.5]:
|
|
# stm.set_ecstm_parameters(C_EDL=20, T=298, lambda_=0.9, V_std=V_std,
|
|
# overpot=0, effective_freq = 1E13, linear_constant=26, threshold_value=1e-5)
|
|
# stm.plot_distributions(E_range=[-7, 4], dE=0.05, sigma=1, fill_area_lower_Fermi_lvl=True,
|
|
# plot_Fermi_Dirac_distib=False, plot_redox_distrib=True)
|
|
# #stm.calculate_2D_ECSTM(z_position=4.5, tip_type='IrCl6', cutoff_in_Angstroms=6)
|
|
# #stm.calculate_2D_ECSTM(z_position=3.0, tip_type='s')
|
|
# #stm.calculate_2D_ECSTM(z_position=3.5, tip_type='pz')
|
|
# stm.calculate_2D_ECSTM(z_position=4.5, tip_type='s')
|
|
|
|
|
|
#stm.set_ecstm_parameters(C_EDL=20, T=298, lambda_=1.63, V_std=-0.6,
|
|
# overpot=0, effective_freq = 1E13, linear_constant=26, threshold_value=1e-5)
|
|
#stm.plot_distributions(E_range=[-7, 4], dE=0.05, sigma=1, fill_area_lower_Fermi_lvl=True,
|
|
# plot_Fermi_Dirac_distib=False, plot_redox_distrib=True)
|
|
#stm.calculate_2D_ECSTM(z_position=3.5, tip_type='oxygen', cutoff_in_Angstroms=5)
|
|
|
|
|
|
#stm.set_ecstm_parameters(C_EDL=20, T=298, lambda_=0.8, V_std=0.1,
|
|
# overpot=0, effective_freq = 1E13, linear_constant=26, threshold_value=1e-5)
|
|
#stm.plot_distributions(E_range=[-7, 4], dE=0.05, sigma=1, fill_area_lower_Fermi_lvl=True,
|
|
# plot_Fermi_Dirac_distib=False, plot_redox_distrib=True)
|
|
#stm.calculate_2D_ECSTM(z_position=5.5, tip_type='RuNH3_6_NNN_plane', cutoff_in_Angstroms=8)
|
|
#stm.calculate_2D_ECSTM(z_position=3.0, tip_type='s')
|
|
#stm.calculate_2D_ECSTM(z_position=3.0, tip_type='pz')
|
|
#stm.calculate_2D_ECSTM(z_position=5.0, tip_type='s')
|
|
#stm.calculate_2D_ECSTM(z_position=5.0, tip_type='pz')
|
|
#stm.calculate_2D_ECSTM(z_position=4.5, tip_type='s')
|
|
#stm.calculate_2D_ECSTM(z_position=4.5, tip_type='pz')
|
|
|
|
stm.set_ecstm_parameters(C_EDL=20, T=298, lambda_=1.2, V_std=0.87,
|
|
overpot=0, effective_freq = 1E13, linear_constant=26, threshold_value=1e-5)
|
|
stm.plot_distributions(E_range=[-7, 4], dE=0.05, sigma=1, fill_area_lower_Fermi_lvl=True,
|
|
plot_Fermi_Dirac_distib=False, plot_redox_distrib=True)
|
|
stm.calculate_2D_ECSTM(z_position=5.5, tip_type='IrCl6', cutoff_in_Angstroms=6)
|
|
#stm.calculate_2D_ECSTM(z_position=3.0, tip_type='s')
|
|
#stm.calculate_2D_ECSTM(z_position=3.0, tip_type='pz')
|
|
#stm.calculate_2D_ECSTM(z_position=5.0, tip_type='s')
|
|
#stm.calculate_2D_ECSTM(z_position=5.0, tip_type='pz')
|
|
#stm.calculate_2D_ECSTM(z_position=4.5, tip_type='s')
|
|
#stm.calculate_2D_ECSTM(z_position=4.5, tip_type='pz')
|
|
|
|
#stm.set_ecstm_parameters(C_EDL=20, T=298, lambda_=0.9, V_std=0.0,
|
|
# overpot=0, effective_freq = 1E13, linear_constant=26, threshold_value=1e-5)
|
|
#stm.plot_distributions(E_range=[-7, 4], dE=0.05, sigma=1, fill_area_lower_Fermi_lvl=True,
|
|
# plot_Fermi_Dirac_distib=False, plot_redox_distrib=True)
|
|
#stm.calculate_2D_ECSTM(z_position=3.0, tip_type='oxygen', cutoff_in_Angstroms=5)
|
|
|
|
|
|
#stm.set_ecstm_parameters(C_EDL=20, T=298, lambda_= 1.0, V_std=-0.5,
|
|
# overpot=0, effective_freq = 1E13, linear_constant=26, threshold_value=1e-5)
|
|
#stm.plot_distributions(E_range=[-7, 4], dE=0.05, sigma=1, fill_area_lower_Fermi_lvl=True,
|
|
# plot_Fermi_Dirac_distib=False, plot_redox_distrib=True)
|
|
#stm.calculate_2D_ECSTM(z_position=3.0, tip_type='oxygen', cutoff_in_Angstroms=5)
|
|
#stm.calculate_2D_ECSTM(z_position=5, tip_type='RuNH3_6_NNN_plane', cutoff_in_Angstroms=8, all_z=True)
|
|
#stm.calculate_2D_ECSTM(z_position=7, tip_type='RuNH3_6_NNN_plane', cutoff_in_Angstroms=8, all_z=True)
|
|
|
|
|
|
#stm.calculate_2D_ECSTM(z_position=4.5, tip_type='oxygen', cutoff_in_Angstroms=5)
|
|
#stm.calculate_2D_ECSTM(z_position=3.5, tip_type='oxygen', cutoff_in_Angstroms=5)
|
|
|
|
##CACLULATE 3D STM IMAGE OPENABLE WITH VESTA PACKAGE###
|
|
#stm.calculate_3D_STM((-0.5, 0.0), tip_type='s', format='cube')
|
|
#stm.calculate_3D_STM((0.0, 0.5), tip_type='s', format='cube')
|
|
|
|
|
|
###CHECK OXYGEN ORBITAL GENERATION###
|
|
#acc_orbitals = stm.generate_acceptor_orbitals('RuNH3_6_NNN_plane', np.shape(stm.WF_ijke)[0:3], z_shift = 20, x_shift=50, y_shift=50)
|
|
#for i, orbitals in enumerate(acc_orbitals):
|
|
# stm.save_as_cube(orbitals, 'RuNH3_NNN'+str(i), 'orbitals')
|
|
#acc_orbitals = stm.generate_acceptor_orbitals('RuNH3_6', np.shape(stm.WF_ijke)[0:3], z_shift = 20, x_shift=50, y_shift=50)
|
|
#for i, orbitals in enumerate(acc_orbitals):
|
|
# stm.save_as_cube(orbitals, 'RuNH3_standart_orient'+str(i), 'orbitals')
|
|
#acc_orbitals = stm.generate_acceptor_orbitals('RuNH3_6_perpendicular', np.shape(stm.WF_ijke)[0:3], z_shift = 20, x_shift=50, y_shift=50)
|
|
#for i, orbitals in enumerate(acc_orbitals):
|
|
# stm.save_as_cube(orbitals, 'RuNH3_perpendicular'+str(i), 'orbitals')
|
|
#acc_orbitals = stm.generate_acceptor_orbitals('oxygen_parallel_y', np.shape(stm.WF_ijke)[0:3], z_shift = 20, x_shift=50, y_shift=50)
|
|
#for i, orbitals in enumerate(acc_orbitals):
|
|
# stm.save_as_vasp(orbitals, 'ox_par_y_orb_'+str(i), 'orbitals')
|
|
|
|
#print(f"Job done! It takes: {time.time()-t} sec") |