Source code for vaspparser.vasp.parser.outcar

# 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