# Copyright (c) Max-Planck-Institut für Eisenforschung GmbH - Computational Materials Design (CM) Department
# Distributed under the terms of "New BSD License", see the LICENSE file.
import re
import warnings
from collections import OrderedDict
from typing import Any, Optional
import numpy as np
import scipy.constants
__author__ = "Sudarsan Surendralal"
__copyright__ = (
"Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - "
"Computational Materials Design (CM) Department"
)
__version__ = "1.0"
__maintainer__ = "Sudarsan Surendralal"
__email__ = "surendralal@mpie.de"
__status__ = "production"
__date__ = "Sep 1, 2017"
KBAR_TO_EVA = (
scipy.constants.physical_constants["joule-electron volt relationship"][0] / 1e22
)
# derives from ValueError, because that was the exception previously raised
[docs]
class OutcarCollectError(ValueError):
pass
[docs]
class Outcar:
"""
This module is used to parse VASP OUTCAR files.
Attributes:
parse_dict (dict): A dictionary with all the useful quantities parsed from an OUTCAR file after from_file() is
executed
"""
[docs]
def __init__(self):
self.parse_dict = {}
[docs]
def from_file(self, filename: str = "OUTCAR"):
"""
Parse and store relevant quantities from the OUTCAR file into parse_dict.
Args:
filename (str): Filename of the OUTCAR file to parse
"""
with open(filename, errors="ignore") as f:
lines = f.readlines()
energies = self.get_total_energies(filename=filename, lines=lines)
energies_int = self.get_energy_without_entropy(filename=filename, lines=lines)
energies_zero = self.get_energy_sigma_0(filename=filename, lines=lines)
scf_energies = self.get_all_total_energies(filename=filename, lines=lines)
n_atoms = self.get_number_of_atoms(filename=filename, lines=lines)
forces = self.get_forces(filename=filename, lines=lines, n_atoms=n_atoms)
positions = self.get_positions(filename=filename, lines=lines, n_atoms=n_atoms)
cells = self.get_cells(filename=filename, lines=lines)
steps = self.get_steps(filename=filename, lines=lines)
temperatures = self.get_temperatures(filename=filename, lines=lines)
time = self.get_time(filename=filename, lines=lines)
fermi_level = self.get_fermi_level(filename=filename, lines=lines)
scf_moments = self.get_dipole_moments(filename=filename, lines=lines)
kin_energy_error = self.get_kinetic_energy_error(filename=filename, lines=lines)
stresses = self.get_stresses(filename=filename, si_unit=False, lines=lines)
n_elect = self.get_nelect(filename=filename, lines=lines)
e_fermi_list, vbm_list, cbm_list = self.get_band_properties(
filename=filename, lines=lines
)
elastic_constants = self.get_elastic_constants(filename=filename, lines=lines)
energy_components = self.get_energy_components(filename=filename, lines=lines)
cpu_time = self.get_cpu_time(filename=filename, lines=lines)
user_time = self.get_user_time(filename=filename, lines=lines)
system_time = self.get_system_time(filename=filename, lines=lines)
elapsed_time = self.get_elapsed_time(filename=filename, lines=lines)
memory_used = self.get_memory_used(filename=filename, lines=lines)
vasp_version = self.get_vasp_version(filename=filename, lines=lines)
try:
(
irreducible_kpoints,
ir_kpt_weights,
plane_waves,
) = self.get_irreducible_kpoints(filename=filename, lines=lines)
except ValueError:
print("irreducible kpoints not parsed !")
irreducible_kpoints = None
ir_kpt_weights = None
plane_waves = None
magnetization, final_magmom_lst = self.get_magnetization(
filename=filename, lines=lines
)
broyden_mixing = self.get_broyden_mixing_mesh(filename=filename, lines=lines)
self.parse_dict["vasp_version"] = vasp_version
self.parse_dict["energies"] = energies
self.parse_dict["energies_int"] = energies_int
self.parse_dict["energies_zero"] = energies_zero
self.parse_dict["scf_energies"] = scf_energies
self.parse_dict["forces"] = forces
self.parse_dict["positions"] = positions
self.parse_dict["cells"] = cells
self.parse_dict["steps"] = steps
self.parse_dict["temperatures"] = temperatures
self.parse_dict["time"] = time
self.parse_dict["fermi_level"] = fermi_level
self.parse_dict["scf_dipole_moments"] = scf_moments
self.parse_dict["kin_energy_error"] = kin_energy_error
self.parse_dict["stresses"] = stresses * KBAR_TO_EVA
self.parse_dict["irreducible_kpoints"] = irreducible_kpoints
self.parse_dict["irreducible_kpoint_weights"] = ir_kpt_weights
self.parse_dict["number_plane_waves"] = plane_waves
self.parse_dict["magnetization"] = magnetization
self.parse_dict["final_magmoms"] = final_magmom_lst
self.parse_dict["broyden_mixing"] = broyden_mixing
self.parse_dict["n_elect"] = n_elect
self.parse_dict["e_fermi_list"] = e_fermi_list
self.parse_dict["vbm_list"] = vbm_list
self.parse_dict["cbm_list"] = cbm_list
self.parse_dict["elastic_constants"] = elastic_constants
self.parse_dict["energy_components"] = energy_components
self.parse_dict["resources"] = {
"cpu_time": cpu_time,
"user_time": user_time,
"system_time": system_time,
"elapsed_time": elapsed_time,
"memory_used": memory_used,
}
try:
self.parse_dict["pressures"] = (
np.average(stresses[:, 0:3], axis=1) * KBAR_TO_EVA
)
except IndexError:
self.parse_dict["pressures"] = np.zeros(len(steps))
[docs]
def to_dict_minimal(self) -> dict:
"""
Return a dictionary containing only the OUTCAR-specific quantities not available
from vasprun.xml. This is used when both output files are present to avoid
duplication.
Returns:
dict: Dictionary with keys: ``kin_energy_error``, ``broyden_mixing``,
``stresses``, ``irreducible_kpoints``, ``irreducible_kpoint_weights``,
``number_plane_waves``, ``energy_components``, ``resources``.
"""
output_dict = {}
unique_quantities = [
"kin_energy_error",
"broyden_mixing",
"stresses",
"irreducible_kpoints",
"irreducible_kpoint_weights",
"number_plane_waves",
"energy_components",
"resources",
]
for key in self.parse_dict:
if key in unique_quantities:
output_dict[key] = self.parse_dict[key]
return output_dict
[docs]
def get_vasp_version(
self, filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> str:
"""
Read the VASP version string from the first line of the OUTCAR file.
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): Lines already read from the file
Returns:
str: VASP version string (e.g. ``"vasp.6.3.2"`` or ``"vasp.5.4.4"``)
"""
lines = _get_lines_from_file(filename=filename, lines=lines)
return lines[0].lstrip().split(sep=" ")[0]
[docs]
def get_positions_and_forces(
self,
filename: str = "OUTCAR",
lines: Optional[list[str]] = None,
n_atoms: Optional[int] = None,
):
"""
Gets the forces and positions for every ionic step from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
n_atoms (int/None): number of ions in OUTCAR
Returns:
[positions, forces] (sequence)
numpy.ndarray: A Nx3xM array of positions in $\AA$
numpy.ndarray: A Nx3xM array of forces in $eV / \AA$
where N is the number of atoms and M is the number of time steps
"""
if n_atoms is None:
n_atoms = self.get_number_of_atoms(filename=filename, lines=lines)
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger="TOTAL-FORCE (eV/Angst)"
)
return self._get_positions_and_forces_parser(
lines=lines,
trigger_indices=trigger_indices,
n_atoms=n_atoms,
pos_flag=True,
force_flag=True,
)
[docs]
def get_positions(
self,
filename: str = "OUTCAR",
lines: Optional[list[str]] = None,
n_atoms: Optional[int] = None,
):
"""
Gets the positions for every ionic step from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
n_atoms (int/None): number of ions in OUTCAR
Returns:
numpy.ndarray: A Nx3xM array of positions in $\AA$
where N is the number of atoms and M is the number of time steps
"""
if n_atoms is None:
n_atoms = self.get_number_of_atoms(filename=filename, lines=lines)
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger="TOTAL-FORCE (eV/Angst)"
)
return self._get_positions_and_forces_parser(
lines=lines,
trigger_indices=trigger_indices,
n_atoms=n_atoms,
pos_flag=True,
force_flag=False,
)
[docs]
def get_forces(
self,
filename: str = "OUTCAR",
lines: Optional[list[str]] = None,
n_atoms: Optional[int] = None,
):
"""
Gets the forces for every ionic step from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
n_atoms (int/None): number of ions in OUTCAR
Returns:
numpy.ndarray: A Nx3xM array of forces in $eV / \AA$
where N is the number of atoms and M is the number of time steps
"""
if n_atoms is None:
n_atoms = self.get_number_of_atoms(filename=filename, lines=lines)
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger="TOTAL-FORCE (eV/Angst)"
)
return self._get_positions_and_forces_parser(
lines=lines,
trigger_indices=trigger_indices,
n_atoms=n_atoms,
pos_flag=False,
force_flag=True,
)
[docs]
def get_cells(self, filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Gets the cell size and shape for every ionic step from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: A 3x3xM array of the cell shape in $\AA$
where M is the number of time steps
"""
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger="VOLUME and BASIS-vectors are now :"
)
return self._get_cells_praser(lines=lines, trigger_indices=trigger_indices)
[docs]
@staticmethod
def get_stresses(
filename: str = "OUTCAR",
lines: Optional[list[str]] = None,
si_unit: bool = True,
):
"""
Args:
filename (str): Input filename
lines (list/None): lines read from the file
si_unit (bool): True SI units are used
Returns:
numpy.ndarray: An array of stress values
"""
trigger_indices, lines = _get_trigger(
lines=lines,
filename=filename,
trigger="FORCE on cell =-STRESS in cart. coord. units (eV):",
)
stress_lst = []
for j in trigger_indices:
# search for '------...' delimiters of the stress table
# setting a constant offset into `lines` does not work, because the number of stress contributions may vary
# depending on the VASP configuration (e.g. with or without van der Waals interactions)
jj = j
while set(lines[jj].strip()) != {"-"}:
jj += 1
jj += 1
# there's two delimiters, so search again
while set(lines[jj].strip()) != {"-"}:
jj += 1
try:
if si_unit:
stress = [float(l) for l in lines[jj + 1].split()[1:7]]
else:
stress = [float(l) for l in lines[jj + 2].split()[2:8]]
except ValueError:
stress = [float("NaN")] * 6
# VASP outputs the stresses in XX, YY, ZZ, XY, YZ, ZX order
# 0, 1, 2, 3, 4, 5
stressm = np.diag(stress[:3])
stressm[0, 1] = stressm[1, 0] = stress[3]
stressm[1, 2] = stressm[2, 1] = stress[4]
stressm[0, 2] = stressm[2, 0] = stress[5]
stress_lst.append(stressm)
return np.array(stress_lst)
[docs]
@staticmethod
def get_irreducible_kpoints(
filename: str = "OUTCAR",
reciprocal: bool = True,
weight: bool = True,
planewaves: bool = True,
lines: Optional[list[str]] = None,
):
"""
Function to extract the irreducible kpoints from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
reciprocal (bool): Get either the reciprocal or the cartesian coordinates
weight (bool): Get the weight assigned to the irreducible kpoints
planewaves (bool): Get the planewaves assigned to the irreducible kpoints
lines (list/None): lines read from the file
Returns:
numpy.ndarray: An array of k-points
"""
kpoint_lst = []
weight_lst = []
planewaves_lst = []
trigger_number_str = "Subroutine IBZKPT returns following result:"
trigger_number_str_alt = "k-points in reciprocal lattice and weights:"
trigger_number_str_total = (
"position of ions in fractional coordinates (direct lattice)"
)
trigger_plane_waves_str = "k-point 1 :"
trigger_plane_waves_alt_str = "k-point 1 :"
trigger_number = 0
trigger_number_alt = 0
trigger_number_alt_total = 0
trigger_plane_waves = 0
lines = _get_lines_from_file(filename=filename, lines=lines)
for i, line in enumerate(lines):
line = line.strip()
if trigger_number_str in line:
trigger_number = int(i)
elif trigger_number_str_alt in line:
trigger_number_alt = int(i) + 1
elif trigger_number_alt != 0 and trigger_number_str_total in line:
trigger_number_alt_total = int(i) - 1
elif (
planewaves
and (
trigger_plane_waves_str in line
or trigger_plane_waves_alt_str in line
)
and "plane waves: " in line
):
trigger_plane_waves = int(i)
if trigger_number != 0:
number_irr_kpoints = int(lines[trigger_number + 3].split()[1])
if reciprocal:
trigger_start = trigger_number + 7
else:
trigger_start = trigger_number + 10 + number_irr_kpoints
else:
trigger_start = trigger_number_alt
number_irr_kpoints = trigger_number_alt_total - trigger_number_alt
for line in lines[trigger_start : trigger_start + number_irr_kpoints]:
line = line.strip()
line = _clean_line(line)
kpoint_lst.append([float(l) for l in line.split()[0:3]])
if weight:
weight_lst.append(float(line.split()[3]))
if planewaves and trigger_plane_waves != 0:
for line in lines[
trigger_plane_waves : trigger_plane_waves + number_irr_kpoints
]:
line = line.strip()
line = _clean_line(line)
planewaves_lst.append(int(line.split()[-1]))
if weight and planewaves:
return np.array(kpoint_lst), np.array(weight_lst), np.array(planewaves_lst)
elif weight:
return np.array(kpoint_lst), np.array(weight_lst)
elif planewaves:
return np.array(kpoint_lst), np.array(planewaves_lst)
else:
return np.array(kpoint_lst)
[docs]
@staticmethod
def get_total_energies(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> np.ndarray:
"""
Gets the total energy for every ionic step from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: A 1xM array of the total energies in $eV$
where M is the number of time steps
"""
def get_total_energies_from_line(line):
return float(_clean_line(line.strip()).split()[-2])
trigger_indices, lines = _get_trigger(
lines=lines,
filename=filename,
trigger="FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)",
)
return np.array(
[get_total_energies_from_line(lines[j + 2]) for j in trigger_indices]
)
[docs]
@staticmethod
def get_energy_without_entropy(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> np.ndarray:
"""
Gets the energy without entropy (E_wo_entrp) for every ionic step from the OUTCAR file.
This corresponds to the ``energy without entropy`` value printed by VASP in the
``FREE ENERGIE OF THE ION-ELECTRON SYSTEM`` block and is equivalent to the
``e_wo_entrp`` tag in vasprun.xml. It equals E_free + T*S, i.e. the total energy
before subtracting the electronic entropy contribution.
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: A 1xM array of the energies without entropy in eV,
where M is the number of ionic steps
"""
def get_energy_without_entropy_from_line(line):
return float(_clean_line(line.strip()).split()[3])
trigger_indices, lines = _get_trigger(
lines=lines,
filename=filename,
trigger="FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)",
)
return np.array(
[
get_energy_without_entropy_from_line(lines[j + 4])
for j in trigger_indices
]
)
[docs]
@staticmethod
def get_energy_sigma_0(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> np.ndarray:
"""
Gets the energy extrapolated to sigma->0 for every ionic step from the OUTCAR file.
VASP prints an extrapolated energy ``energy(sigma->0)`` which removes the
broadening-dependent entropy contribution. This is useful for comparing energies
computed with finite electronic temperature (ISMEAR, SIGMA) as it approximates
the T=0 energy. Corresponds to ``e_0_energy`` in vasprun.xml.
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: A 1xM array of the sigma->0 extrapolated energies in eV,
where M is the number of ionic steps
"""
def get_energy_sigma_0_from_line(line):
return float(_clean_line(line.strip()).split()[-1])
trigger_indices, lines = _get_trigger(
lines=lines,
filename=filename,
trigger="FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)",
)
return np.array(
[get_energy_sigma_0_from_line(lines[j + 4]) for j in trigger_indices]
)
[docs]
@staticmethod
def get_ediel_sol(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> np.ndarray:
"""
Gets the dielectric solvation energy (Ediel_sol) for every ionic step from the OUTCAR file.
This energy is only present when an implicit solvation model is used (e.g. with the
VASPsol or VASP built-in solvation via ``LSOL = .TRUE.``). It represents the
electrostatic interaction energy between the solute and the implicit solvent.
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: A 1xM array of the solvation energies in eV,
where M is the number of ionic steps
"""
def get_ediel_sol_from_line(line):
return float(_clean_line(line.strip()).split()[-1])
trigger_indices, lines = _get_trigger(
lines=lines,
filename=filename,
trigger="Solvation Ediel_sol = ",
)
return np.array([get_ediel_sol_from_line(lines[j]) for j in trigger_indices])
[docs]
@staticmethod
def get_all_total_energies(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> list[np.ndarray]:
"""
Gets the energy at every electronic step
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
list: A list of energie for every electronic step at every ionic step
"""
ind_ionic_lst, lines = _get_trigger(
trigger="FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)",
filename=filename,
lines=lines,
return_lines=True,
)
ind_elec_lst = _get_trigger(
trigger="free energy TOTEN =",
filename=None,
lines=lines,
return_lines=False,
)
ind_combo_lst = _split_indices(
ind_ionic_lst=ind_ionic_lst, ind_elec_lst=ind_elec_lst
)
return [
np.array(
[float(_clean_line(lines[ind].strip()).split()[-2]) for ind in ind_lst]
)
for ind_lst in ind_combo_lst
]
[docs]
@staticmethod
def get_magnetization(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Gets the magnetization
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
list: A list with the mgnetization values
"""
ionic_trigger = "FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)"
electronic_trigger = "eigenvalue-minimisations"
nion_trigger = "NIONS ="
mag_lst: list[np.ndarray] = []
local_spin_trigger = False
n_atoms = None
mag_dict: dict[str, list[list[float]]] = {}
mag_dict["x"] = []
mag_dict["y"] = []
mag_dict["z"] = []
lines = _get_lines_from_file(filename=filename, lines=lines)
istep_energies: list[float | list[float]] = []
final_magmom_lst: list[Any] = []
for i, line in enumerate(lines):
line = line.strip()
if ionic_trigger in line:
mag_lst.append(np.array(istep_energies))
istep_energies = []
if "Atomic Wigner-Seitz radii" in line:
local_spin_trigger = True
if electronic_trigger in line:
try:
line = lines[i + 2].split("magnetization")[-1]
if line != " \n":
spin_str_lst = line.split()
spin_str_len = len(spin_str_lst)
if spin_str_len == 1:
ene: float | list[float] = float(line)
elif spin_str_len == 3:
ene = [
float(spin_str_lst[0]),
float(spin_str_lst[1]),
float(spin_str_lst[2]),
]
else:
warnings.warn(
"Unrecognized spin configuration.", stacklevel=2
)
return mag_lst, final_magmom_lst
istep_energies.append(ene)
except ValueError:
warnings.warn(
"Something went wrong in parsing the magnetization",
stacklevel=2,
)
if n_atoms is None and nion_trigger in line:
n_atoms = int(line.split(nion_trigger)[-1])
if local_spin_trigger:
if n_atoms is None:
continue
try:
for _ind_dir, direc in enumerate(["x", "y", "z"]):
if f"magnetization ({direc})" in line:
mag_dict[direc].append(
[
float(lines[i + 4 + atom_index].split()[-1])
for atom_index in range(n_atoms)
]
)
except ValueError:
warnings.warn(
"Something went wrong in parsing the magnetic moments",
stacklevel=2,
)
if len(mag_dict["x"]) > 0:
if len(mag_dict["y"]) == 0:
final_mag = np.array(mag_dict["x"])
else:
if n_atoms is None:
return mag_lst, final_magmom_lst
n_ionic_steps = np.array(mag_dict["x"]).shape[0]
final_mag = np.abs(np.zeros((n_ionic_steps, n_atoms, 3)))
final_mag[:, :, 0] = np.array(mag_dict["x"])
final_mag[:, :, 1] = np.array(mag_dict["y"])
final_mag[:, :, 2] = np.array(mag_dict["z"])
final_magmom_lst = final_mag.tolist()
return mag_lst, final_magmom_lst
[docs]
@staticmethod
def get_broyden_mixing_mesh(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
):
"""
Gets the Broyden mixing mesh size
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
int: Mesh size
"""
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger="gives a total of "
)
if len(trigger_indices) > 0:
line_ngx = lines[trigger_indices[0] - 2]
else:
warnings.warn(
"Unable to parse the Broyden mixing mesh. Returning 0 instead",
stacklevel=2,
)
return 0
# Exclude all alphabets, and spaces. Then split based on '='
str_list = re.sub(
r"[a-zA-Z]", r"", line_ngx.replace(" ", "").replace("\n", "")
).split("=")
return np.prod([int(val) for val in str_list[1:]])
[docs]
@staticmethod
def get_temperatures(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Gets the temperature at each ionic step (applicable for MD)
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: An array of temperatures in Kelvin
"""
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger="kin. lattice EKIN_LAT= "
)
temperatures: list[float] = []
if len(trigger_indices) > 0:
for j in trigger_indices:
line = lines[j].strip()
line = _clean_line(line)
output_string = line.split("temperature")[-1].split()[0]
try:
temperatures.append(float(output_string))
except ValueError:
warnings.warn(
f"Temperature too high. Vasp output: {line}", stacklevel=2
)
temperatures.append(np.nan)
else:
return np.zeros(
len(
_get_trigger(
lines=lines,
trigger="FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)",
return_lines=False,
)
)
)
return np.array(temperatures)
[docs]
@staticmethod
def get_steps(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: Steps during the simulation
"""
nblock_regex = re.compile(r"NBLOCK =\s+(\d+);")
trigger = "FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)"
steps = 0
nblock = None
lines = _get_lines_from_file(filename=filename, lines=lines)
for line in lines:
if trigger in line:
steps += 1
if nblock is None and (match := nblock_regex.search(line)):
nblock = int(match[1])
if nblock is None:
nblock = 1
return np.arange(0, steps * nblock, nblock)
[docs]
def get_time(self, filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Time after each simulation step (for MD)
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: An array of time values in fs
"""
potim_trigger = "POTIM ="
potim = 1.0
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
if potim_trigger in line:
line = line.strip()
line = _clean_line(line)
potim = float(line.split(potim_trigger)[1].strip().split()[0])
break
return potim * self.get_steps(filename)
[docs]
@staticmethod
def get_kinetic_energy_error(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> float:
"""
Get the kinetic energy error
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
float: The kinetic energy error in eV
"""
trigger = "kinetic energy error for atom="
e_kin_err = []
n_species_list = []
nion_trigger = "ions per type ="
tot_kin_error = 0.0
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if trigger in line:
e_kin_err.append(float(line.split()[5]))
if nion_trigger in line:
n_species_list = [
float(val) for val in line.split(nion_trigger)[-1].strip().split()
]
if len(n_species_list) > 0 and len(n_species_list) == len(e_kin_err):
tot_kin_error = np.sum(np.array(n_species_list) * np.array(e_kin_err))
return tot_kin_error
[docs]
@staticmethod
def get_fermi_level(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> Optional[float]:
"""
Getting the Fermi-level (Kohn_Sham) from the OUTCAR file
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
float: The Kohn-Sham Fermi level in eV
"""
trigger = "E-fermi :"
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger=trigger
)
if len(trigger_indices) != 0:
try:
return float(lines[trigger_indices[-1]].split(trigger)[-1].split()[0])
except ValueError:
return None
else:
return None
[docs]
@staticmethod
def get_dipole_moments(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Get the electric dipole moment at every electronic step
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
list: A list of dipole moments in (eA) for each electronic step
"""
moment_trigger = "dipolmoment"
istep_trigger = "FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)"
dip_moms = []
lines = _get_lines_from_file(filename=filename, lines=lines)
istep_mom: list[np.ndarray] = []
for _i, line in enumerate(lines):
line = line.strip()
if istep_trigger in line:
dip_moms.append(np.array(istep_mom))
istep_mom = []
if moment_trigger in line:
line = _clean_line(line)
mom = np.array([float(val) for val in line.split()[1:4]])
istep_mom.append(mom)
return dip_moms
[docs]
@staticmethod
def get_nelect(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> Optional[float]:
"""
Returns the number of electrons in the simulation
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
float: The number of electrons in the simulation
"""
nelect_trigger = "NELECT"
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if nelect_trigger in line:
return float(line.split()[2])
return None
[docs]
@staticmethod
def get_cpu_time(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Returns the total CPU time in seconds
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
float: CPU time in seconds
"""
nelect_trigger = "Total CPU time used (sec):"
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if nelect_trigger in line:
return float(line.split()[-1])
[docs]
@staticmethod
def get_user_time(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Returns the User time in seconds
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
float: User time in seconds
"""
nelect_trigger = "User time (sec):"
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if nelect_trigger in line:
return float(line.split()[-1])
[docs]
@staticmethod
def get_system_time(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Returns the system time in seconds
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
float: system time in seconds
"""
nelect_trigger = "System time (sec):"
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if nelect_trigger in line:
return float(line.split()[-1])
[docs]
@staticmethod
def get_elapsed_time(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Returns the elapsed time in seconds
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
float: elapsed time in seconds
"""
nelect_trigger = "Elapsed time (sec):"
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if nelect_trigger in line:
return float(line.split()[-1])
[docs]
@staticmethod
def get_memory_used(filename: str = "OUTCAR", lines: Optional[list[str]] = None):
"""
Returns the maximum memory used during the simulation in kB
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
float: Maximum memory used in kB
"""
nelect_trigger = "Maximum memory used (kb):"
lines = _get_lines_from_file(filename=filename, lines=lines)
for _i, line in enumerate(lines):
line = line.strip()
if nelect_trigger in line:
return float(line.split()[-1])
[docs]
@staticmethod
def get_number_of_atoms(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
) -> int:
"""
Returns the number of ions in the simulation
Args:
filename (str): OUTCAR filename
lines (list/None): lines read from the file
Returns:
int: The number of ions in the simulation
"""
ions_trigger = "NIONS ="
trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger=ions_trigger
)
if len(trigger_indices) != 0:
return int(lines[trigger_indices[0]].split(ions_trigger)[-1])
else:
raise OutcarCollectError(
"Failed to read number of atoms, can't find NIONS!"
)
[docs]
@staticmethod
def get_band_properties(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
):
"""
Extract the Fermi level, valence band maximum (VBM) and conduction band minimum (CBM)
at every ionic step from the OUTCAR file.
The values are derived from the ``band No. band energies occupation`` tables
printed in the OUTCAR after each self-consistent cycle.
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): Lines already read from the file
Returns:
tuple:
numpy.ndarray: Fermi levels (eV) for each ionic step
numpy.ndarray: VBM values (eV), shape (n_spins, n_ionic_steps)
numpy.ndarray: CBM values (eV), shape (n_spins, n_ionic_steps)
"""
fermi_trigger = "E-fermi"
fermi_trigger_indices, lines = _get_trigger(
lines=lines, filename=filename, trigger=fermi_trigger
)
fermi_level_list = []
vbm_level_dict: OrderedDict[int, list[float]] = OrderedDict()
cbm_level_dict: OrderedDict[int, list[float]] = OrderedDict()
for ind in fermi_trigger_indices:
fermi_level_list.append(float(lines[ind].strip().split()[2]))
band_trigger = "band No. band energies occupation"
is_spin_polarized = False
for n, ind in enumerate(fermi_trigger_indices):
if n == len(fermi_trigger_indices) - 1:
trigger_indices, lines_new = _get_trigger(
lines=lines[ind:-1], filename=filename, trigger=band_trigger
)
else:
trigger_indices, lines_new = _get_trigger(
lines=lines[ind : fermi_trigger_indices[n + 1]],
filename=filename,
trigger=band_trigger,
)
band_data = []
for ind in trigger_indices:
if "spin component" in lines_new[ind - 3]:
is_spin_polarized = True
for line in lines_new[ind + 1 :]:
data = line.strip().split()
# This if "Fermi" bypass needs to exist because of VASP changing it's OUTCAR format after 6.1.0
# In all versions prior, searching "Fermi" will only yield 2 mentions in the OUTCAR
# In the new versions, a Fermi energy is printed immediately after each spin-component k-point text block:
# i.e. Fermi energy: XXXXX
# This breaks the (old) parser, and thus this bypass is necessary to skip this last line at each spin component block
# Ugly and hacky, but parsing the OUTCAR is a ugly and hacky endeavour in general
if "Fermi" in data:
continue
if len(data) != 3:
break
band_data.append([float(d) for d in data[1:]])
if is_spin_polarized:
band_data_per_spin = [
np.array(band_data[0 : int(len(band_data) / 2)]).tolist(),
np.array(band_data[int(len(band_data) / 2) :]).tolist(),
]
else:
band_data_per_spin = [band_data]
for spin, band_data in enumerate(band_data_per_spin):
if spin in cbm_level_dict:
pass
else:
cbm_level_dict[spin] = []
if spin in vbm_level_dict:
pass
else:
vbm_level_dict[spin] = []
if len(band_data) > 0:
band_energy, band_occ = [
np.array(band_data)[:, i] for i in range(2)
]
args = np.argsort(band_energy)
band_occ = band_occ[args]
band_energy = band_energy[args]
cbm_bool = np.abs(band_occ) < 1e-6
if any(cbm_bool):
cbm_level_dict[spin].append(
band_energy[np.abs(band_occ) < 1e-6][0]
)
else:
cbm_level_dict[spin].append(band_energy[-1])
# If spin channel is completely empty, setting vbm=cbm
if all(cbm_bool):
vbm_level_dict[spin].append(cbm_level_dict[spin][-1])
else:
vbm_level_dict[spin].append(band_energy[~cbm_bool][-1])
return (
np.array(fermi_level_list),
np.array(list(vbm_level_dict.values())),
np.array(list(cbm_level_dict.values())),
)
[docs]
@staticmethod
def get_elastic_constants(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
):
"""
Read the elastic stiffness tensor from the OUTCAR file.
The elastic constants are computed by VASP when ``IBRION=6`` is set in the INCAR.
VASP prints the full 6x6 Voigt-notation elastic moduli matrix under the heading
``TOTAL ELASTIC MODULI (kBar)``; this method converts those values to GPa.
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): Lines already read from the file
Returns:
numpy.ndarray or None: A 6x6 array of elastic constants in GPa in Voigt notation
(order: xx, yy, zz, xy, yz, xz), or None if no elastic constants are found.
"""
lines = _get_lines_from_file(filename=filename, lines=lines)
trigger_indices = _get_trigger(
lines=lines,
filename=filename,
trigger="TOTAL ELASTIC MODULI (kBar)",
return_lines=False,
)
if len(trigger_indices) != 1:
return None
else:
start_index = trigger_indices[0] + 3
end_index = start_index + 6
elastic_constants = []
for line in lines[start_index:end_index]:
elastic_constants.append(line.split()[1:])
elastic_GPa = np.array(elastic_constants, dtype=float) / 10
return elastic_GPa
@staticmethod
def _get_positions_and_forces_parser(
lines: list[str],
trigger_indices: list[int],
n_atoms: int,
pos_flag: bool = True,
force_flag: bool = True,
):
"""
Parser to get the forces and or positions for every ionic step from the OUTCAR file
Args:
lines (list): lines read from the file
trigger_indices (list): list of line indices where the trigger was found.
n_atoms (int): number of atoms
pos_flag (bool): parse position
force_flag (bool): parse forces
Returns:
[positions, forces] (sequence)
numpy.ndarray: A Nx3xM array of positions in $\AA$
numpy.ndarray: A Nx3xM array of forces in $eV / \AA$
where N is the number of atoms and M is the number of time steps
"""
positions = []
forces = []
for j in trigger_indices:
pos = []
force = []
for line in lines[j + 2 : j + n_atoms + 2]:
line = line.strip()
line = _clean_line(line)
if pos_flag:
pos.append([float(l) for l in line.split()[0:3]])
if force_flag:
force.append([float(l) for l in line.split()[3:]])
forces.append(force)
positions.append(pos)
if pos_flag and force_flag:
return np.array(positions), np.array(forces)
elif pos_flag:
return np.array(positions)
elif force_flag:
return np.array(forces)
return np.array([])
@staticmethod
def _get_cells_praser(lines: list[str], trigger_indices: list[int]):
"""
Parser to get the cell size and shape for every ionic step from the OUTCAR file
Args:
lines (list): lines read from the file
trigger_indices (list): list of line indices where the trigger was found.
n_atoms (int): number of atoms
Returns:
numpy.ndarray: A 3x3xM array of the cell shape in $\AA$
where M is the number of time steps
"""
cells = []
try:
for j in trigger_indices:
cell = []
for line in lines[j + 5 : j + 8]:
line = line.strip()
line = _clean_line(line)
cell.append([float(l) for l in line.split()[0:3]])
cells.append(cell)
return np.array(cells)
except ValueError:
warnings.warn(
"Unable to parse the cells from the OUTCAR file", stacklevel=2
)
return
[docs]
@staticmethod
def get_energy_components(
filename: str = "OUTCAR", lines: Optional[list[str]] = None
):
"""
Gets the individual components of the free energy energy for every electronic step from the OUTCAR file
alpha Z PSCENC = -0.19957337
Ewald energy TEWEN = -73.03212173
-Hartree energ DENC = -0.10933240
-exchange EXHF = 0.00000000
-V(xc)+E(xc) XCENC = -26.17018410
PAW double counting = 168.82497547 -136.88269783
entropy T*S EENTRO = -0.00827174
eigenvalues EBANDS = 10.35379785
atomic energy EATOM = 53.53616173
Solvation Ediel_sol = 0.00000000
Args:
filename (str): Filename of the OUTCAR file to parse
lines (list/None): lines read from the file
Returns:
numpy.ndarray: A 1xM array of the total energies in $eV$
where M is the number of time steps
"""
ind_ionic_lst, lines = _get_trigger(
trigger="FREE ENERGIE OF THE ION-ELECTRON SYSTEM (eV)",
filename=filename,
lines=lines,
return_lines=True,
)
ind_elec_lst = _get_trigger(
trigger="Free energy of the ion-electron system (eV)",
filename=None,
lines=lines,
return_lines=False,
)
ind_combo_lst = _split_indices(
ind_ionic_lst=ind_ionic_lst, ind_elec_lst=ind_elec_lst
)
try:
return [
np.array(
[
np.hstack(
[
(
float(lines[ind + i].split()[-1])
if i != 7
else [
float(lines[ind_lst[-1] + 7].split()[-2]),
float(lines[ind_lst[-1] + 7].split()[-1]),
]
)
for i in range(2, 12)
]
)
for ind in ind_lst
]
).T
for ind_lst in ind_combo_lst
]
except ValueError:
return []
def _clean_line(line: str) -> str:
return line.replace("-", " -")
def _get_trigger(
trigger: str,
filename: Optional[str] = None,
lines: Optional[list[str]] = None,
return_lines: bool = True,
):
"""
Find the lines where a specific trigger appears.
Args:
trigger (str): string pattern to search for
lines (list/None): list of lines
filename (str/None): file to read lines from
Returns:
list: indicies of the lines where the trigger string was found and list of lines
"""
lines = _get_lines_from_file(filename=filename, lines=lines)
trigger_indicies = [i for i, line in enumerate(lines) if trigger in line.strip()]
if return_lines:
return trigger_indicies, lines
else:
return trigger_indicies
def _split_indices(
ind_ionic_lst: list[int], ind_elec_lst: list[int]
) -> list[np.ndarray]:
"""
Combine ionic pattern matches and electronic pattern matches
Args:
ind_ionic_lst (list): indices of lines which matched the iconic pattern
ind_elec_lst (list): indices of lines which matched the electronic pattern
Returns:
list: nested list of electronic pattern matches within an ionic pattern match
"""
ind_elec_array = np.array(ind_elec_lst)
return [
(
ind_elec_array[(ind_elec_array < j2) & (j1 < ind_elec_array)]
if j1 < j2
else ind_elec_array[(ind_elec_array < j2)]
)
for j1, j2 in zip(np.roll(ind_ionic_lst, 1), ind_ionic_lst)
]
def _get_lines_from_file(filename: Optional[str], lines: Optional[list[str]] = None):
"""
If lines is None read the lines from the file with the filename filename.
Args:
filename (str): file to read lines from
lines (list/ None): list of lines
Returns:
list: list of lines
"""
if lines is None:
if filename is None:
raise ValueError("Either filename or lines must be provided")
with open(filename, errors="ignore") as f:
lines = f.readlines()
return lines