__author__ = "Logan Lang"
__maintainer__ = "Logan Lang"
__email__ = "lllang@mix.wvu.edu"
__date__ = "March 31, 2020"
import re
import copy
import os
import math
import xml.etree.ElementTree as ET
import numpy as np
from pyprocar.core import DensityOfStates, Structure, ElectronicBandStructure, KPath
HARTREE_TO_EV = 27.211386245988 #eV/Hartree
[docs]class QEParser():
"""The class is used to parse Quantum Expresso files.
The most important objects that comes from this parser are the .ebs and .dos
Parameters
----------
dirname : str, optional
Directory path to where calculation took place, by default ""
scf_in_filename : str, optional
The scf filename, by default "scf.in"
bands_in_filename : str, optional
The bands filename in the case of a band structure calculation, by default "bands.in"
pdos_in_filename : str, optional
The pdos filename in the case of a density ofstates calculation, by default "pdos.in"
kpdos_in_filename : str, optional
The kpdos filename, by default "kpdos.in"
atomic_proj_xml : str, optional
The atomic projection xml name. This is located in the where the outdir is and in the {prefix}.save directory, by default "atomic_proj.xml"
"""
[docs] def __init__(self,
dirname:str = "",
scf_in_filename:str = "scf.in",
bands_in_filename:str = "bands.in",
pdos_in_filename:str = "pdos.in",
kpdos_in_filename:str = "kpdos.in",
atomic_proj_xml:str = "atomic_proj.xml",
):
# Handles the pathing to the files
self.dirname, prefix, xml_root, atomic_proj_xml_filename, pdos_in_filename,bands_in_filename,proj_out_filename = self._initialize_filenames(dirname, scf_in_filename, bands_in_filename,pdos_in_filename)
# Parsing structual and calculation type information
self._parse_efermi(main_xml_root=xml_root)
self._parse_magnetization(main_xml_root=xml_root)
self._parse_structure(main_xml_root=xml_root)
self._parse_band_structure_tag(main_xml_root=xml_root)
self._parse_symmetries(main_xml_root=xml_root)
# Parsing projections spd array and spd phase arrays
if os.path.exists(atomic_proj_xml_filename):
self._parse_wfc_mapping(proj_out_filename=proj_out_filename)
self._parse_atomic_projections(atomic_proj_xml_filename=atomic_proj_xml_filename)
# Parsing density of states files
if os.path.exists(pdos_in_filename):
self.dos = self._parse_pdos(pdos_in_filename=pdos_in_filename,dirname=dirname)
else:
self.dos = None
# Parsing information related to the bandstructure calculations kpath and klabels
self.kticks = None
self.knames = None
self.kpath = None
if xml_root.findall(".//input/control_variables/calculation")[0].text == "bands":
self.isBandsCalc = True
with open(bands_in_filename, "r") as f:
self.bandsIn = f.read()
self._get_kpoint_labels()
self.ebs = ElectronicBandStructure(
kpoints=self.kpoints,
n_kx=self.nkx,
n_ky=self.nky,
n_kz=self.nkz,
bands=self.bands,
projected=self._spd2projected(self.spd),
efermi=self.efermi,
kpath=self.kpath,
projected_phase=self._spd2projected(self.spd_phase),
labels=self.orbital_names[:-1],
reciprocal_lattice=self.reciprocal_lattice,
)
return None
[docs] def kpoints_cart(self):
"""Returns the kpoints in cartesian coordinates
Returns
-------
np.ndarray
Kpoints in cartesian coordinates
"""
# cart_kpoints self.kpoints = self.kpoints*(2*np.pi /self.alat)
# Converting back to crystal basis
cart_kpoints = self.kpoints.dot(self.reciprocal_lattice)
return cart_kpoints
@property
def species(self):
"""Returns the species of the calculation
Returns
-------
List
Returns a list of string or atomic numbers[int]
"""
return self.initial_structure.species
@property
def structures(self):
"""Returns a list of pyprocar.core.Structure
Returns
-------
List
Returns a list of pyprocar.core.Structure
"""
symbols = [x.strip() for x in self.ions]
structures = []
st = Structure(
atoms=symbols,
lattice=self.direct_lattice,
fractional_coordinates=self.atomic_positions,
rotations=self.rotations
)
structures.append(st)
return structures
@property
def structure(self):
"""Returns a the last element of a list of pyprocar.core.Structure
Returns
-------
pyprocar.core.Structure
Returns a the last element of a list of pyprocar.core.Structure
"""
return self.structures[-1]
@property
def initial_structure(self):
"""Returns a the first element of a list of pyprocar.core.Structure
Returns
-------
pyprocar.core.Structure
Returns a the first element of a list of pyprocar.core.Structure
"""
return self.structures[0]
@property
def final_structure(self):
"""Returns a the last element of a list of pyprocar.core.Structure
Returns
-------
pyprocar.core.Structure
Returns a the last element of a list of pyprocar.core.Structure
"""
return self.structures[-1]
def _parse_pdos(self,pdos_in_filename,dirname):
"""Helper method to parse the pdos files
Parameters
----------
pdos_in_filename : str
The pdos.in filename
dirname : str
The directory path where the calculation took place.
Returns
-------
pyprocar.core.DensityOfStates
The density of states object for the calculation
"""
with open(pdos_in_filename, "r") as f:
pdos_in = f.read()
self.pdos_prefix = re.findall("filpdos\s*=\s*'(.*)'", pdos_in)[0]
self.proj_prefix = re.findall("filproj\s*=\s*'(.*)'", pdos_in)[0]
# Parsing total density of states
energies, total_dos = self._parse_dos_total(dos_total_filename=f"{dirname}{os.sep}{self.pdos_prefix}.pdos_tot")
self.n_energies=len(energies)
# Finding all the density of states projections files
wfc_filenames = self._parse_available_wfc_filenames(dirname = self.dirname)
projected_dos = self._parse_dos_projections(wfc_filenames=wfc_filenames, n_energy = len(energies))
# print(projected_labels)
dos = DensityOfStates(energies=energies,
total=total_dos,
efermi=self.efermi,
projected=projected_dos,
interpolation_factor = 1)
return dos
def _parse_dos_total(self, dos_total_filename ):
"""Helper method to parse the dos total file
Parameters
----------
dos_total_filename : str
The dos total filename
Returns
-------
Tupole
Returns a tuple with energies and the total dos arrays
"""
with open(dos_total_filename) as f:
tmp_text = f.readlines()
header = tmp_text[0]
dos_text = ''.join(tmp_text[1:])
# Strip ending spaces away. Avoind empty string at the end
raw_dos_blocks_by_energy = dos_text.rstrip().split('\n')
n_energies = len(raw_dos_blocks_by_energy)
energies = np.zeros(shape=(n_energies))
# total_dos = np.zeros(shape=(n_energies, self.n_spin))
total_dos = np.zeros(shape=(self.n_spin,n_energies))
for ienergy, raw_dos_block_by_energy in enumerate(raw_dos_blocks_by_energy):
energies[ienergy] = float(raw_dos_block_by_energy.split()[0])
# Covers colinear spin-polarized. This is because these is a difference in energies
if self.n_spin == 2:
total_dos[:,ienergy] = [ float(val) for val in raw_dos_block_by_energy.split()[-self.n_spin:] ]
# Covers colinear non-spin-polarized and non-colinear. This is because the energies are the same
else:
total_dos[0,ienergy] = float(raw_dos_block_by_energy.split()[2])
energies -= self.efermi
return energies, total_dos
def _parse_dos_projections(self, wfc_filenames, n_energy):
"""Parse the dos projection files using efficient array operations."""
n_principal_number = 1
projected_dos_array = np.zeros((self.n_atoms, n_principal_number, self.n_orbitals, self.n_spin, n_energy))
for filename in wfc_filenames:
self._validate_file(filename)
# Some pdos files are not non-colinear.
# In the version of the qe code where we get the spin projections.
# Non-colinear pdos files should have the word 'j' in the filename.
if not self._is_non_colinear_file(filename) and self.is_non_colinear:
continue
# Determine the parsing method based on calculation type
if self.is_non_colinear:
self._parse_non_colinear(filename, projected_dos_array)
else:
self._parse_colinear(filename, projected_dos_array)
return projected_dos_array
def _is_non_colinear_file(self, filename):
"""Determine if the file corresponds to non-colinear calculations."""
file_tag = filename.split('_')[-1]
return 'j' in file_tag
def _validate_file(self, filename):
if not os.path.exists(filename):
raise ValueError(f'ERROR: pdos file not found: {filename}')
def _read_file_content(self, filename):
with open(filename) as f:
# Check if spin component projections are present
lines=f.readlines()[1:]
spin_projections_present = 'lsigma' in lines[1]
if spin_projections_present:
pdos=self._parse_spin_components_lines(lines)
elif self.is_non_colinear:
pdos=self._parse_noncolinear_pdos(lines)
else:
pdos=self._parse_colinear_pdos(lines)
return pdos
def _parse_spin_components_lines(self, lines):
n_orbitals=len(lines[0].split()) - 2
pdos=np.zeros(shape=(self.n_energies, n_orbitals, self.n_spin))
for i_energy in range(self.n_energies):
iblock_start=i_energy*6
total_line=lines[iblock_start].split()[2:]
x_line=lines[iblock_start+2].split()[1:]
y_line=lines[iblock_start+3].split()[1:]
z_line=lines[iblock_start+4].split()[1:]
pdos[i_energy, : ,0] = [float(tot) for tot in total_line]
pdos[i_energy, : ,1] = [float(x) for x in x_line]
pdos[i_energy, : ,2] = [float(y) for y in y_line]
pdos[i_energy, : ,3] = [float(z) for z in z_line]
# Move energy to the last axis
pdos = np.moveaxis(pdos, 0, -1)
return pdos
def _parse_colinear_pdos(self,lines):
# The energy column is first, the sum totals by spin channel
n_orbitals=len(lines[0].split()) - self.n_spin - 1
n_orbitals //= self.n_spin
pdos=np.zeros(shape=(self.n_energies, n_orbitals, self.n_spin))
col_start= 1 + self.n_spin
for i_energy in range(self.n_energies):
# Skip energies and sum over projections
values=lines[i_energy].split()[col_start:]
if self.n_spin == 1:
pdos[i_energy, : , 0] = values
else:
pdos[i_energy, : , 0] = values[::2]
pdos[i_energy, : , 1] = values[1::2]
# Move energy to the last axis
pdos = np.moveaxis(pdos, 0, -1)
return pdos
def _parse_noncolinear_pdos(self,lines):
# The energy column is first, the sum totals by spin channel
n_orbitals=len(lines[0].split()) - 1 - 1
n_orbitals //= 1
pdos=np.zeros(shape=(self.n_energies, n_orbitals, 4))
col_start= 1 + 1
for i_energy in range(self.n_energies):
# Skip energies and sum over projections
values=lines[i_energy].split()[col_start:]
pdos[i_energy, : , 0] = values
# Move energy to the last axis
pdos = np.moveaxis(pdos, 0, -1)
return pdos
def _extract_file_info(self, filename):
atom_num = int(re.findall(r"#(\d*)", filename)[0]) - 1
wfc_name = re.findall(r"atm#\d*\(([a-zA-Z0-9]*)\)", filename)[0]
orbital_name = filename.split('(')[-1][0]
total_angular_momentum=None
if self.is_non_colinear:
tmp_str=filename.split('_')[-1]
total_angular_momentum=float(tmp_str.strip('j').strip(')'))
return atom_num, wfc_name, orbital_name,total_angular_momentum
def _parse_non_colinear(self, filename, projected_dos_array):
pdos_data = self._read_file_content(filename)
atom_num, wfc_name, orbital_name, total_angular_momentum = self._extract_file_info(filename)
orbital_nums, _ = self._get_orbital_info_non_colinear(orbital_name, total_angular_momentum)
if not orbital_nums:
raise ValueError(f'ERROR: orbital_nums is empty for {filename}')
projected_dos_array[atom_num, 0, orbital_nums, :, :self.n_energies] += pdos_data
def _parse_colinear(self, filename, projected_dos_array):
pdos_data = self._read_file_content(filename)
atom_num, wfc_name, orbital_name,total_angular_momentum = self._extract_file_info(filename)
orbital_nums, _ = self._get_orbital_info_colinear(orbital_name)
if not orbital_nums:
raise ValueError(f'ERROR: orbital_nums is empty for {filename}')
projected_dos_array[atom_num, 0, orbital_nums, : , :self.n_energies] += pdos_data
def _get_orbital_info_non_colinear(self, l_orbital_name, tot_ang_mom):
orbital_info = {
's': {0.5: ([0, 1], np.linspace(-0.5, 0.5, 2))},
'p': {0.5: ([2, 3], np.linspace(-0.5, 0.5, 2)),
1.5: ([4, 5, 6, 7], np.linspace(-1.5, 1.5, 4))},
'd': {0.5: ([2, 3], np.linspace(-0.5, 0.5, 2)),
1.5: ([8, 9, 10, 11], np.linspace(-1.5, 1.5, 4)),
2.5: ([12, 13, 14, 15, 16, 17], np.linspace(-2.5, 2.5, 6))}
}
return orbital_info.get(l_orbital_name, {}).get(tot_ang_mom, ([], []))
def _get_orbital_info_colinear(self, orbital_name):
orbital_info = {
's': ([0], np.linspace(0, 0, 1)),
'p': ([1, 2, 3], np.linspace(-1, 1, 3)),
'd': ([4, 5, 6, 7, 8], np.linspace(-2, 2, 5))
}
return orbital_info.get(orbital_name, ([], []))
def _get_kpoint_labels(self):
"""
This method will parse the bands.in file to get the kpath information.
"""
# Parsing klabels
self.ngrids = []
kmethod = re.findall("K_POINTS[\s\{]*([a-z_]*)[\s\{]*", self.bandsIn)[0]
self.discontinuities = []
if kmethod == "crystal":
numK = int(re.findall("K_POINTS.*\n([0-9]*)", self.bandsIn)[0])
raw_khigh_sym = re.findall(
"K_POINTS.*\n\s*[0-9]*.*\n" + numK * "(.*)\n*", self.bandsIn
)[0]
tickCountIndex = 0
self.knames = []
self.kticks = []
for x in raw_khigh_sym:
if len(x.split()) == 5:
self.knames.append("%s" % x.split()[4].replace("!", ""))
self.kticks.append(tickCountIndex)
tickCountIndex += 1
self.nhigh_sym = len(self.knames)
elif kmethod == "crystal_b":
self.nhigh_sym = int(re.findall("K_POINTS.*\n([0-9]*)", self.bandsIn)[0])
raw_khigh_sym = re.findall(
"K_POINTS.*\n.*\n" + self.nhigh_sym * "(.*)\n*",
self.bandsIn,
)[0]
self.kticks = []
self.high_symmetry_points = np.zeros(shape=(self.nhigh_sym, 3))
tick_Count = 1
for ihs in range(self.nhigh_sym):
# In QE cyrstal_b mode, the user is able to specify grid on last high symmetry point.
# QE just uses 1 for the last high symmetry point.
grid_current = int(raw_khigh_sym[ihs].split()[3])
if ihs < self.nhigh_sym - 2:
self.ngrids.append(grid_current)
elif ihs == self.nhigh_sym - 1:
self.ngrids.append(grid_current+1)
elif ihs == self.nhigh_sym:
continue
self.kticks.append(tick_Count - 1)
tick_Count += grid_current
raw_ticks = re.findall(
"K_POINTS.*\n\s*[0-9]*\s*[0-9]*.*\n" + self.nhigh_sym * ".*!(.*)\n*",
self.bandsIn,
)[0]
if len(raw_ticks) != self.nhigh_sym:
self.knames = [str(x) for x in range(self.nhigh_sym)]
else:
self.knames = [
"%s" % (x.replace(",", "").replace("vlvp1d", "").replace(" ", ""))
for x in raw_ticks
]
# Formating to conform with Kpath class
self.special_kpoints = np.zeros(shape = (len(self.kticks) -1 ,2,3) )
self.modified_knames = []
for itick in range(len(self.kticks)):
if itick != len(self.kticks) - 1:
self.special_kpoints[itick,0,:] = self.kpoints[self.kticks[itick]]
self.special_kpoints[itick,1,:] = self.kpoints[self.kticks[itick+1]]
self.modified_knames.append([self.knames[itick], self.knames[itick+1] ])
has_time_reversal = True
self.kpath = KPath(
knames=self.modified_knames,
special_kpoints=self.special_kpoints,
kticks = self.kticks,
ngrids=self.ngrids,
has_time_reversal=has_time_reversal,
)
def _initialize_filenames(self, dirname, scf_in, bands_in_filename, pdos_in_filename):
"""This helper method handles pathing to the to locate files
Parameters
----------
dirname : str
The directory path where the calculation is
scf_in : str
The input scf filename
bands_in_filename : str
The input bands filename
pdos_in_filename : str
The input pdos filename
Returns
-------
Tuple
Returns a tuple of important pathing information.
Mainly, the directory path is prepended to the filenames.
"""
if dirname != "":
dirname = dirname + os.sep
else:
dirname = ""
scf_in_file_path = os.path.join(dirname,scf_in)
with open(scf_in_file_path, "r") as f:
scf_in = f.read()
outdir = re.findall("outdir\s*=\s*'\S*?([A-Za-z]*)'", scf_in)[0]
prefix = re.findall("prefix\s*=\s*'(.*)'", scf_in)[0]
xml_filename = prefix + ".xml"
atomic_proj_xml = os.path.join(dirname,outdir,prefix + ".save","atomic_proj.xml")
if not os.path.exists(atomic_proj_xml):
atomic_proj_xml = os.path.join(dirname,"atomic_proj.xml")
output_xml=os.path.join(dirname,outdir,xml_filename)
if not os.path.exists(output_xml):
output_xml = os.path.join(dirname,xml_filename)
tree = ET.parse(output_xml)
root = tree.getroot()
prefix = root.findall(".//input/control_variables/prefix")[0].text
pdos_in_filename = f"{dirname}{pdos_in_filename}"
bands_in_filename = f"{dirname}{bands_in_filename}"
dirname = dirname
if os.path.exists(f"{dirname}{os.sep}kpdos.out"):
proj_out_filename = f"{dirname}{os.sep}kpdos.out"
if os.path.exists(f"{dirname}{os.sep}pdos.out"):
proj_out_filename = f"{dirname}{os.sep}pdos.out"
return dirname, prefix, root, atomic_proj_xml, pdos_in_filename,bands_in_filename,proj_out_filename
def _parse_available_wfc_filenames(self, dirname):
"""Helper method to parse the projection filename from the pdos.out file
Parameters
----------
dirname : str
The directory name where the calculation is.
Returns
-------
List
Returns a list of projection file names
"""
wfc_filenames = []
tmp_wfc_filenames = []
atms_wfc_num = []
# Parsing projection filnames for identification information
for file in os.listdir(f"{self.dirname}"):
if (file.startswith(self.pdos_prefix) and not
file.endswith(".pdos_tot") and not
file.endswith(".lowdin") and not
file.endswith(".projwfc_down") and not
file.endswith(".projwfc_up")and not
file.endswith(".xml")):
filename = f"{self.dirname}{os.sep}{file}"
tmp_wfc_filenames.append(filename )
atm_num = int(re.findall("_atm#([0-9]*)\(.*",filename)[0])
wfc_num = int(re.findall("_wfc#([0-9]*)\(.*",filename)[0])
wfc = re.findall("_wfc#[0-9]*\(([_A-Za-z0-9.]*)\).*",filename)[0]
atm = re.findall("_atm#[0-9]*\(([A-Za-z]*[0-9]*)\).*",filename)[0]
atms_wfc_num.append((atm_num,atm,wfc_num,wfc))
# sort density of states projections files by atom number
sorted_file_num = sorted(atms_wfc_num, key= lambda a: a[0])
for index in sorted_file_num:
wfc_filenames.append(f"{self.dirname}{os.sep}{self.pdos_prefix}.pdos_atm#{index[0]}({index[1]})_wfc#{index[2]}({index[3]})")
return wfc_filenames
def _parse_wfc_mapping(self, proj_out_filename):
"""Helper method which creates a mapping between wfc number and the orbtial and atom numbers
Parameters
----------
proj_out_filename : str
The proj out filename
Returns
-------
None
None
"""
with open(proj_out_filename) as f:
proj_out = f.read()
raw_wfc = re.findall('(?<=read\sfrom\spseudopotential\sfiles).*\n\n([\S\s]*?)\n\n(?=\sk\s=)', proj_out)[0]
wfc_list = raw_wfc.split('\n')
self.wfc_mapping={}
# print(self.orbitals)
for i, wfc in enumerate(wfc_list):
iwfc = int(re.findall('(?<=state\s#)\s*(\d*)',wfc)[0])
iatm = int(re.findall('(?<=atom)\s*(\d*)',wfc)[0])
l_orbital_type_index = int(re.findall('(?<=l=)\s*(\d*)',wfc)[0])
if self.is_non_colinear:
j_orbital_type_index = float(re.findall('(?<=j=)\s*([-\d.]*)',wfc)[0])
m_orbital_type_index = float(re.findall('(?<=m_j=)\s*([-\d.]*)',wfc)[0])
tmp_orb_dict = {"l" : self._convert_lorbnum_to_letter(lorbnum=l_orbital_type_index),
"j" : j_orbital_type_index,
"m" : m_orbital_type_index}
# print(self._convert_lorbnum_to_letter(lorbnum=l_orbital_type_index))
else:
m_orbital_type_index = int(re.findall('(?<=m=)\s*(\d*)',wfc)[0])
tmp_orb_dict = {"l" : l_orbital_type_index , "m" : m_orbital_type_index}
iorb = 0
for iorbital, orb in enumerate(self.orbitals):
if tmp_orb_dict == orb:
iorb = iorbital
self.wfc_mapping.update({f"wfc_{iwfc}":{"orbital" : iorb, "atom" : iatm}})
return None
def _parse_atomic_projections(self,atomic_proj_xml_filename):
"""A Helper method to parse the atomic projection xml file
Parameters
----------
atomic_proj_xml_filename : str
The atomic_proj.xml filename
Returns
-------
None
None
"""
atmProj_tree = ET.parse(atomic_proj_xml_filename)
atm_proj_root = atmProj_tree.getroot()
root_header = atm_proj_root.findall(".//HEADER")[0]
nbnd = int(root_header.get("NUMBER_OF_BANDS"))
nk = int(root_header.get("NUMBER_OF_K-POINTS"))
nwfc = int(root_header.get("NUMBER_OF_ATOMIC_WFC"))
norb = self.n_orbitals
natm = self.n_atoms
nspin_channels = int(root_header.get("NUMBER_OF_SPIN_COMPONENTS"))
nspin_projections = self.n_spin
# The indices are to match the format of the from PROCAR format.
# In it there is an extra 2 columns for orbitals for the ion index and the total
# Also there is and extra row for the totals
self.spd = np.zeros(shape = (self.n_k, self.n_band , self.n_spin ,self.n_atoms+1,self.n_orbitals + 2,))
self.spd_phase = np.zeros(
shape=(
self.spd.shape
),
dtype=np.complex_,
)
bands=self._parse_bands_tag(atm_proj_root, nk, nbnd, nspin_channels)
kpoints=self._parse_kpoints_tag(atm_proj_root, nk, nspin_channels)
projs, projs_phase=self._parse_projections_tag(atm_proj_root, nk, nbnd, natm, norb, nspin_channels, nspin_projections)
# maping the projections to the spd array. The spd array is the output of the PROCAR file
self.spd[:,:,:,:-1,1:-1] += projs[:,:,:,:,:]
self.spd_phase[:,:,:,:-1,1:-1] += projs_phase[:,:,:,:,:]
# Adding atom index. This is a vasp output thing
for ions in range(self.ionsCount):
self.spd[:, :, :, ions, 0] = ions + 1
# The following fills the totals for the spd array. Again this is a vasp output thing.
self.spd[:, :, :, :, -1] = np.sum(self.spd[:, :, :, :, 1:-1], axis=4)
self.spd[:, :, :, -1, :] = np.sum(self.spd[:, :, :, :-1, :], axis=3)
self.spd[:, :, :, -1, 0] = 0
return None
def _parse_bands_tag(self,atm_proj_root, nk, nbnd, nspins):
bands=np.zeros(shape=(nk,nbnd,nspins))
results=atm_proj_root.findall(".//EIGENSTATES/E")
# For spin-polarized calculations, there are two spin channels.
# They add them by first adding the spin up and then the spin down
# I break this down with the folloiwng indexing
spin_reuslts=[results[i*nk : (i+1)*nk ] for i in range(nspins)]
for ispin,spin_result in enumerate(spin_reuslts):
for ik,result in enumerate(spin_result):
bands_per_kpoint=result.text.split()
bands[ik,:,ispin]=bands_per_kpoint
return bands
def _parse_kpoints_tag(self,atm_proj_root, nk, nspins):
kpoints=np.zeros(shape=(nk,3))
kpoint_tags=atm_proj_root.findall(".//EIGENSTATES/K-POINT")
# For spin-polarized calculations, there are two spin channels.
# They add them by first adding the spin up and then the spin down
# I break this down with the folloiwng indexing
spin_reuslts=[kpoint_tags[i*nk : (i+1)*nk ] for i in range(nspins)]
for ispin,spin_result in enumerate(spin_reuslts):
for ik,kpoint_tag in enumerate(spin_result):
kpoint=kpoint_tag.text.split()
kpoints[ik,:]=kpoint
return kpoints
def _parse_projections_tag(self,atm_proj_root, nk, nbnd, natm, norb, nspin_channels, nspin_projections):
projs=np.zeros(shape=(nk,nbnd,nspin_projections,natm,norb))
projs_phase=np.zeros(shape=(nk,nbnd,nspin_projections,natm,norb), dtype=np.complex_)
proj_tags=atm_proj_root.findall(".//EIGENSTATES/PROJS")
# For spin-polarized calculations, there are two spin channels.
# They add them by first adding the spin up and then the spin down
# I break this down with the folloiwng indexing
spin_reuslts=[proj_tags[i*nk : (i+1)*nk ] for i in range(nspin_channels)]
for ispin,spin_result in enumerate(spin_reuslts):
for ik,proj_tag in enumerate(spin_result):
atm_wfs_tags=proj_tag.findall('ATOMIC_WFC')
for atm_wfs_tag in atm_wfs_tags:
iwfc = int(atm_wfs_tag.get('index'))
iorb = self.wfc_mapping[f"wfc_{iwfc}"]["orbital"]
iatm = self.wfc_mapping[f"wfc_{iwfc}"]["atom"] - 1
band_projections=atm_wfs_tag.text.strip().split('\n')
for iband, band_projection in enumerate(band_projections):
real = float(band_projection.split()[0])
imag = float(band_projection.split()[1])
comp = complex(real , imag)
comp_squared = np.absolute(comp)**2
projs_phase[ik,iband,ispin,iatm,iorb] = complex(real , imag)
projs[ik,iband,ispin,iatm,iorb] = comp_squared
atm_sigma_wfs_tags=proj_tag.findall('ATOMIC_SIGMA_PHI')
if atm_sigma_wfs_tags:
spin_x_projections = [atm_sigma_wfs_tag for atm_sigma_wfs_tag in atm_sigma_wfs_tags if atm_sigma_wfs_tag.get('ipol') == '1']
spin_y_projections = [atm_sigma_wfs_tag for atm_sigma_wfs_tag in atm_sigma_wfs_tags if atm_sigma_wfs_tag.get('ipol') == '2']
spin_z_projections = [atm_sigma_wfs_tag for atm_sigma_wfs_tag in atm_sigma_wfs_tags if atm_sigma_wfs_tag.get('ipol') == '3']
spin_projections=[spin_x_projections,spin_y_projections,spin_z_projections]
for i_spin_component,spin_projection_tags in enumerate(spin_projections):
for spin_projection_tag in spin_projection_tags:
iwfc = int(spin_projection_tag.get('index'))
iorb = self.wfc_mapping[f"wfc_{iwfc}"]["orbital"]
iatm = self.wfc_mapping[f"wfc_{iwfc}"]["atom"] - 1
band_projections=spin_projection_tag.text.strip().split('\n')
for iband, band_projection in enumerate(band_projections):
real = float(band_projection.split()[0])
imag = float(band_projection.split()[1])
comp = complex(real , imag)
comp_squared = np.absolute(comp)**2
# Move spin index by 1 to match the order in the spd array
# First index should be total,
# second, third, and fourth should be x,y,z, respoectively
projs_phase[ik,iband,i_spin_component+1,iatm,iorb] = complex(real , imag)
projs[ik,iband,i_spin_component+1,iatm,iorb] = real
return projs, projs_phase
def _parse_structure(self,main_xml_root):
"""A helper method to parse the structure tag of the main xml file
Parameters
----------
main_xml_root : xml.etree.ElementTree.Element
The main xml Element
Returns
-------
None
None
"""
self.nspecies = len(main_xml_root.findall(".//output/atomic_species")[0])
self.composition = { species.attrib['name'] : 0 for species in main_xml_root.findall(".//output/atomic_species")[0] }
self.species_list = list(self.composition.keys())
self.ionsCount = int(main_xml_root.findall(".//output/atomic_structure")[0].attrib['nat'])
self.alat = float(main_xml_root.findall(".//output/atomic_structure")[0].attrib['alat'])
self.ions = []
for ion in main_xml_root.findall(".//output/atomic_structure/atomic_positions")[0]:
self.ions.append(ion.attrib['name'][:2])
self.composition[ ion.attrib['name']] += 1
self.n_atoms = len(self.ions)
self.atomic_positions = np.array([ ion.text.split() for ion in main_xml_root.findall(".//output/atomic_structure/atomic_positions")[0]],dtype = float)
# in a.u
self.direct_lattice = np.array([ acell.text.split() for acell in main_xml_root.findall(".//output/atomic_structure/cell")[0] ],dtype = float)
self.reciprocal_lattice = (2 * np.pi /self.alat) * np.array([ acell.text.split() for acell in main_xml_root.findall(".//output/basis_set/reciprocal_lattice")[0] ],dtype = float)
return None
def _parse_symmetries(self,main_xml_root):
"""A helper method to parse the symmetries tag of the main xml file
Parameters
----------
main_xml_root : xml.etree.ElementTree.Element
The main xml Element
Returns
-------
None
None
"""
self.nsym = int(main_xml_root.findall(".//output/symmetries/nsym")[0].text)
self.nrot = int(main_xml_root.findall(".//output/symmetries/nrot")[0].text)
self.spg = int(main_xml_root.findall(".//output/symmetries/space_group")[0].text)
self.nsymmetry = len(main_xml_root.findall(".//output/symmetries/symmetry"))
self.rotations = np.zeros(shape = (self.nsymmetry ,3,3))
for isymmetry,symmetry_operation in enumerate(main_xml_root.findall(".//output/symmetries/symmetry")):
symmetry_matrix = np.array(symmetry_operation.findall(".//rotation")[0].text.split(),dtype = float).reshape(3,3).T
self.rotations[isymmetry,:,:] = symmetry_matrix
return None
def _parse_magnetization(self,main_xml_root):
"""A helper method to parse the magnetization tag of the main xml file
Parameters
----------
main_xml_root : xml.etree.ElementTree.Element
The main xml Element
Returns
-------
None
None
"""
is_non_colinear = str2bool(main_xml_root.findall(".//output/magnetization/noncolin")[0].text)
is_spin_calc = str2bool(main_xml_root.findall(".//output/magnetization/lsda")[0].text)
is_spin_orbit_calc = str2bool(main_xml_root.findall(".//output/magnetization/spinorbit")[0].text)
# The calcuulation is non-colinear
if is_non_colinear :
n_spin = 4
orbitals = [
{"l": 's', "j": 0.5, "m": -0.5},
{"l": 's', "j": 0.5, "m": 0.5},
{"l": 'p', "j": 0.5, "m": -0.5},
{"l": 'p', "j": 0.5, "m": 0.5},
{"l": 'p', "j": 1.5, "m": -1.5},
{"l": 'p', "j": 1.5, "m": -0.5},
{"l": 'p', "j": 1.5, "m": -0.5},
{"l": 'p', "j": 1.5, "m": 1.5},
{"l": 'd', "j": 1.5, "m": -1.5},
{"l": 'd', "j": 1.5, "m": -0.5},
{"l": 'd', "j": 1.5, "m": -0.5},
{"l": 'd', "j": 1.5, "m": 1.5},
{"l": 'd', "j": 2.5, "m": -2.5},
{"l": 'd', "j": 2.5, "m": -1.5},
{"l": 'd', "j": 2.5, "m": -0.5},
{"l": 'd', "j": 2.5, "m": 0.5},
{"l": 'd', "j": 2.5, "m": 1.5},
{"l": 'd', "j": 2.5, "m": 2.5},
]
orbitalNames = []
for orbital in orbitals:
tmp_name = ''
for key,value in orbital.items():
# print(key,value)
if key != 'l':
tmp_name = tmp_name + key + str(value)
else:
tmp_name = tmp_name + str(value) + '_'
orbitalNames.append(tmp_name)
# The calcuulation is colinear
else:
# colinear spin or non spin polarized
if is_spin_calc:
n_spin = 2
else:
n_spin = 1
orbitals = [
{"l": 0, "m": 1},
{"l": 1, "m": 3},
{"l": 1, "m": 1},
{"l": 1, "m": 2},
{"l": 2, "m": 5},
{"l": 2, "m": 3},
{"l": 2, "m": 1},
{"l": 2, "m": 2},
{"l": 2, "m": 4},
]
orbitalNames = [
"s",
"py",
"pz",
"px",
"dxy",
"dyz",
"dz2",
"dxz",
"dx2",
"tot",
]
self.is_non_colinear = is_non_colinear
self.is_spin_calc = is_spin_calc
self.is_spin_orbit_calc = is_spin_orbit_calc
self.n_spin = n_spin
self.orbitals = orbitals
self.n_orbitals = len(orbitals)
self.orbital_names = orbitalNames
return None
def _parse_band_structure_tag(self,main_xml_root):
"""A helper method to parse the band_structure tag of the main xml file
Parameters
----------
main_xml_root : xml.etree.ElementTree.Element
The main xml Element
Returns
-------
None
None
"""
self.nkx=None
self.nky=None
self.nkz=None
self.nk1=None
self.nk2=None
self.nk3=None
monkhorst_tag=main_xml_root.findall(".//output/band_structure/starting_k_points")[0][0]
if 'monkhorst_pack' in monkhorst_tag.tag:
self.nkx = float(monkhorst_tag.attrib['nk1'])
self.nky = float(monkhorst_tag.attrib['nk2'])
self.nkz = float(monkhorst_tag.attrib['nk3'])
self.nk1 = float(monkhorst_tag.attrib['k1'])
self.nk2 = float(monkhorst_tag.attrib['k2'])
self.nk3 = float(monkhorst_tag.attrib['k3'])
self.nks = int(main_xml_root.findall(".//output/band_structure/nks")[0].text)
self.atm_wfc = int(main_xml_root.findall(".//output/band_structure/num_of_atomic_wfc")[0].text)
self.nelec = float(main_xml_root.findall(".//output/band_structure/nelec")[0].text)
if self.n_spin == 2:
self.n_band = int(main_xml_root.findall(".//output/band_structure/nbnd_up")[0].text)
self.nbnd_up = int(main_xml_root.findall(".//output/band_structure/nbnd_up")[0].text)
self.nbnd_down = int(main_xml_root.findall(".//output/band_structure/nbnd_dw")[0].text)
self.bands = np.zeros(shape = (self.nks, self.n_band , 2))
self.kpoints = np.zeros(shape = (self.nks, 3))
self.weights = np.zeros(shape = (self.nks))
self.occupations = np.zeros(shape = (self.nks, self.n_band,2))
band_structure_element = main_xml_root.findall(".//output/band_structure")[0]
for ikpoint, kpoint_element in enumerate(main_xml_root.findall(".//output/band_structure/ks_energies")):
self.kpoints[ikpoint,:] = np.array(kpoint_element.findall(".//k_point")[0].text.split(),dtype = float)
self.weights[ikpoint] = np.array(kpoint_element.findall(".//k_point")[0].attrib["weight"], dtype = float)
self.bands[ikpoint, : ,0] = HARTREE_TO_EV * np.array(kpoint_element.findall(".//eigenvalues")[0].text.split(),dtype = float)[:self.nbnd_up]
self.occupations[ikpoint, : ,0] = np.array(kpoint_element.findall(".//occupations")[0].text.split(), dtype = float)[:self.nbnd_up]
self.bands[ikpoint, : ,1] = HARTREE_TO_EV * np.array(kpoint_element.findall(".//eigenvalues")[0].text.split(),dtype = float)[self.nbnd_down:]
self.occupations[ikpoint, : ,1] = np.array(kpoint_element.findall(".//occupations")[0].text.split(), dtype = float)[self.nbnd_down:]
# For non-spin-polarized and non colinear
else:
self.n_band = int(main_xml_root.findall(".//output/band_structure/nbnd")[0].text)
self.bands = np.zeros(shape = (self.nks, self.n_band, 1))
self.kpoints = np.zeros(shape = (self.nks, 3))
self.weights = np.zeros(shape = (self.nks))
self.occupations = np.zeros(shape = (self.nks, self.n_band))
for ikpoint, kpoint_element in enumerate(main_xml_root.findall(".//output/band_structure/ks_energies")):
self.kpoints[ikpoint,:] = np.array(kpoint_element.findall(".//k_point")[0].text.split(),dtype = float)
self.weights[ikpoint] = np.array(kpoint_element.findall(".//k_point")[0].attrib["weight"], dtype = float)
self.bands[ikpoint, : , 0] = HARTREE_TO_EV * np.array(kpoint_element.findall(".//eigenvalues")[0].text.split(),dtype = float)
self.occupations[ikpoint, : ] = np.array(kpoint_element.findall(".//occupations")[0].text.split(), dtype = float)
# Multiply in 2pi/alat
self.kpoints = self.kpoints*(2*np.pi /self.alat)
# Converting back to crystal basis
self.kpoints = np.around(self.kpoints.dot(np.linalg.inv(self.reciprocal_lattice)),decimals=8)
self.n_k = len(self.kpoints)
self.kpointsCount = len(self.kpoints)
self.bandsCount = self.n_band
return None
def _spd2projected(self, spd, nprinciples=1):
"""
Helpermethod to project the spd array to the projected array
which will be fed into pyprocar.coreElectronicBandStructure object
Parameters
----------
spd : np.ndarray
The spd array from the earlier parse. This has a structure simlar to the PROCAR output in vasp
Has the shape [n_kpoints,n_band,n_spins,n-orbital,n_atoms]
nprinciples : int, optional
The prinicipal quantum numbers, by default 1
Returns
-------
np.ndarray
The projected array. Has the shape [n_kpoints,n_band,n_atom,n_principal,n-orbital,n_spin]
"""
# This function is for VASP
# non-pol and colinear
# spd is formed as (nkpoints,nbands, nspin, natom+1, norbital+2)
# natom+1 > last column is total
# norbital+2 > 1st column is the number of atom last is total
# non-colinear
# spd is formed as (nkpoints,nbands, nspin +1 , natom+1, norbital+2)
# natom+1 > last column is total
# norbital+2 > 1st column is the number of atom last is total
# nspin +1 > last column is total
if spd is None:
return None
natoms = spd.shape[3] - 1
nkpoints = spd.shape[0]
nbands = spd.shape[1]
nspins = spd.shape[2]
norbitals = spd.shape[4] - 2
# if spd.shape[2] == 4:
# nspins = 3
# else:
# nspins = spd.shape[2]
# if nspins == 2:
# nbands = int(spd.shape[1] / 2)
# else:
# nbands = spd.shape[1]
projected = np.zeros(
shape=(nkpoints, nbands, natoms, nprinciples, norbitals, nspins),
dtype=spd.dtype,
)
temp_spd = spd.copy()
# (nkpoints,nbands, nspin, natom, norbital)
temp_spd = np.swapaxes(temp_spd, 2, 4)
# (nkpoints,nbands, norbital , natom , nspin)
temp_spd = np.swapaxes(temp_spd, 2, 3)
# (nkpoints,nbands, natom, norbital, nspin)
# projected[ikpoint][iband][iatom][iprincipal][iorbital][ispin]
# if nspins == 3:
# # Used if self.spins==3
# projected[:, :, :, 0, :, :] = temp_spd[:, :, :-1, 1:-1, :]
# # Used if self.spins == 4
# # projected[:, :, :, 0, :, :] = temp_spd[:, :, :-1, 1:-1, 1:]
if nspins == 2:
projected[:, :, :, 0, :, 0] = temp_spd[:, :, :-1, 1:-1, 0]
projected[:, :, :, 0, :, 1] = temp_spd[:, :, :-1, 1:-1, 1]
else:
projected[:, :, :, 0, :, :] = temp_spd[:, :, :-1, 1:-1, :]
return projected
def _parse_efermi(self,main_xml_root):
"""A helper method to parse the band_structure tag of the main xml file for the fermi energy
Parameters
----------
main_xml_root : xml.etree.ElementTree.Element
The main xml Element
Returns
-------
None
None
"""
self.efermi = float(main_xml_root.findall(".//output/band_structure/fermi_energy")[0].text) * HARTREE_TO_EV
return None
def _convert_lorbnum_to_letter(self, lorbnum):
"""A helper method to convert the lorb number to the letter format
Parameters
----------
lorbnum : int
The number of the l orbital
Returns
-------
str
The l orbital name
"""
lorb_mapping = {0:'s',1:'p',2:'d',3:'f'}
return lorb_mapping[lorbnum]
def str2bool(v):
"""Converts a string of a boolean to an actual boolean
Parameters
----------
v : str
The string of the boolean value
Returns
-------
boolean
The boolean value
"""
return v.lower() in ("true")