Source code for vaspparser.vasp.output

import contextlib
import os
import posixpath
import warnings
from typing import Callable, Optional, Sequence, Union

import numpy as np
from ase.atoms import Atoms

from vaspparser.dft.bader import Bader
from vaspparser.dft.waves.electronic import ElectronicStructure
from vaspparser.vasp.parser.oszicar import Oszicar
from vaspparser.vasp.parser.outcar import Outcar, OutcarCollectError
from vaspparser.vasp.procar import Procar
from vaspparser.vasp.structure import read_atoms, vasp_sorter
from vaspparser.vasp.vasprun import Vasprun as Vr
from vaspparser.vasp.vasprun import VasprunError, VasprunWarning
from vaspparser.vasp.volumetric_data import VaspVolumetricData


class Output:
    """
    Handles the output from a VASP simulation.

    Attributes:
        electronic_structure: Gives the electronic structure of the system
        electrostatic_potential: Gives the electrostatic/local potential of the system
        charge_density: Gives the charge density of the system
    """

    def __init__(self):
        self._structure = None
        self.outcar = Outcar()
        self.oszicar = Oszicar()
        self.generic_output = GenericOutput()
        self.description = (
            "This contains all the output static from this particular vasp run"
        )
        self.charge_density = VaspVolumetricData()
        self.electrostatic_potential = VaspVolumetricData()
        self.procar = Procar()
        self.electronic_structure = ElectronicStructure()
        self.vp_new = Vr()

    @property
    def structure(self):
        """
        Getter for the output structure
        """
        return self._structure

    @structure.setter
    def structure(self, atoms: Atoms):
        """
        Setter for the output structure
        """
        self._structure = atoms

[docs] def collect( self, directory: str = os.getcwd(), sorted_indices: Optional[np.ndarray] = None, es_class=ElectronicStructure, ): """ Collects output from the working directory Args: directory (str): Path to the directory sorted_indices (np.array/None): """ if sorted_indices is None: sorted_indices = vasp_sorter(self.structure) files_present = os.listdir(directory) log_dict = {} vasprun_working, outcar_working = False, False if not ("OUTCAR" in files_present or "vasprun.xml" in files_present): raise OSError("Either the OUTCAR or vasprun.xml files need to be present") if "OSZICAR" in files_present: self.oszicar.from_file(filename=posixpath.join(directory, "OSZICAR")) if "OUTCAR" in files_present: try: self.outcar.from_file(filename=posixpath.join(directory, "OUTCAR")) outcar_working = True except OutcarCollectError as e: warnings.warn( f"OUTCAR present, but could not be parsed: {e}!", stacklevel=2 ) outcar_working = False if "vasprun.xml" in files_present: try: with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") self.vp_new.from_file( filename=posixpath.join(directory, "vasprun.xml") ) if any(isinstance(warn.category, VasprunWarning) for warn in w): warnings.warn( "vasprun.xml parsed but with some inconsistencies. " "Check vasp output to be sure", VasprunWarning, stacklevel=2, ) except VasprunError: warnings.warn( "Unable to parse the vasprun.xml file. Will attempt to get data from OUTCAR", stacklevel=2, ) else: # If parsing the vasprun file does not throw an error, then set to True vasprun_working = True if outcar_working: log_dict["temperature"] = self.outcar.parse_dict["temperatures"] log_dict["stresses"] = self.outcar.parse_dict["stresses"] log_dict["pressures"] = self.outcar.parse_dict["pressures"] log_dict["elastic_constants"] = self.outcar.parse_dict["elastic_constants"] self.generic_output.dft_log_dict["n_elect"] = self.outcar.parse_dict[ "n_elect" ] if len(self.outcar.parse_dict["magnetization"]) > 0: magnetization = np.array( self.outcar.parse_dict["magnetization"], dtype=object ) final_magmoms = np.array( self.outcar.parse_dict["final_magmoms"], dtype=object ) # magnetization[sorted_indices] = magnetization.copy() if len(final_magmoms) != 0: if len(final_magmoms.shape) == 3: final_magmoms[:, sorted_indices, :] = final_magmoms.copy() else: final_magmoms[:, sorted_indices] = final_magmoms.copy() self.generic_output.dft_log_dict["magnetization"] = ( magnetization.tolist() ) self.generic_output.dft_log_dict["final_magmoms"] = ( final_magmoms.tolist() ) self.generic_output.dft_log_dict["e_fermi_list"] = self.outcar.parse_dict[ "e_fermi_list" ] self.generic_output.dft_log_dict["vbm_list"] = self.outcar.parse_dict[ "vbm_list" ] self.generic_output.dft_log_dict["cbm_list"] = self.outcar.parse_dict[ "cbm_list" ] if vasprun_working: log_dict["forces"] = self.vp_new.vasprun_dict["forces"] log_dict["cells"] = self.vp_new.vasprun_dict["cells"] log_dict["volume"] = np.linalg.det(self.vp_new.vasprun_dict["cells"]) # The vasprun parser also returns the energies printed again after the final SCF cycle under the key # "total_energies", but due to a bug in the VASP output, the energies reported there are wrong in Vasp 5.*; # instead use the last energy from the scf cycle energies # BUG link: https://ww.vasp.at/forum/viewtopic.php?p=19242 try: # bug report is not specific to which Vasp5 versions are affected; be safe and workaround for all of # them is_vasp5 = self.vp_new.vasprun_dict["generator"]["version"].startswith( "5." ) except KeyError: # in case the parser didn't read the version info is_vasp5 = True if is_vasp5: log_dict["energy_pot"] = np.array( [e[-1] for e in self.vp_new.vasprun_dict["scf_fr_energies"]] ) else: # total energies refers here to the total energy of the electronic system, not the total system of # electrons plus (potentially) moving ions; hence this is the energy_pot log_dict["energy_pot"] = self.vp_new.vasprun_dict["total_fr_energies"] if "kinetic_energies" in self.vp_new.vasprun_dict: log_dict["energy_tot"] = ( log_dict["energy_pot"] + self.vp_new.vasprun_dict["kinetic_energies"] ) else: log_dict["energy_tot"] = log_dict["energy_pot"] log_dict["steps"] = np.arange(len(log_dict["energy_tot"])) log_dict["positions"] = self.vp_new.vasprun_dict["positions"] log_dict["forces"][:, sorted_indices] = log_dict["forces"].copy() log_dict["positions"][:, sorted_indices] = log_dict["positions"].copy() log_dict["positions"] = np.einsum( "nij,njk->nik", log_dict["positions"], log_dict["cells"] ) # log_dict["scf_energies"] = self.vp_new.vasprun_dict["scf_energies"] # log_dict["scf_dipole_moments"] = self.vp_new.vasprun_dict["scf_dipole_moments"] self.electronic_structure = self.vp_new.get_electronic_structure( es_class=es_class, ) if self.electronic_structure.grand_dos_matrix is not None: self.electronic_structure.grand_dos_matrix[ :, :, :, sorted_indices, : ] = self.electronic_structure.grand_dos_matrix[:, :, :, :, :].copy() if self.electronic_structure.resolved_densities is not None: self.electronic_structure.resolved_densities[ :, sorted_indices, :, : ] = self.electronic_structure.resolved_densities[:, :, :, :].copy() self.structure.positions = log_dict["positions"][-1] self.structure.set_cell(log_dict["cells"][-1]) self.generic_output.dft_log_dict["potentiostat_output"] = ( self.vp_new.get_potentiostat_output() ) valence_charges_orig = self.vp_new.get_valence_electrons_per_atom() valence_charges = valence_charges_orig.copy() valence_charges[sorted_indices] = valence_charges_orig self.generic_output.dft_log_dict["valence_charges"] = valence_charges elif outcar_working: # log_dict = self.outcar.parse_dict.copy() if len(self.outcar.parse_dict["energies"]) == 0: raise VaspCollectError("Error in parsing OUTCAR") log_dict["energy_tot"] = self.outcar.parse_dict["energies"] log_dict["temperature"] = self.outcar.parse_dict["temperatures"] log_dict["stresses"] = self.outcar.parse_dict["stresses"] log_dict["pressures"] = self.outcar.parse_dict["pressures"] log_dict["forces"] = self.outcar.parse_dict["forces"] log_dict["positions"] = self.outcar.parse_dict["positions"] log_dict["forces"][:, sorted_indices] = log_dict["forces"].copy() log_dict["positions"][:, sorted_indices] = log_dict["positions"].copy() if len(log_dict["positions"].shape) != 3 or log_dict["positions"].shape[ 1 ] != len(sorted_indices): raise VaspCollectError("Improper OUTCAR parsing") if len(log_dict["forces"].shape) != 3 or log_dict["forces"].shape[1] != len( sorted_indices ): raise VaspCollectError("Improper OUTCAR parsing") log_dict["time"] = self.outcar.parse_dict["time"] log_dict["steps"] = self.outcar.parse_dict["steps"] log_dict["cells"] = self.outcar.parse_dict["cells"] log_dict["volume"] = np.array( [np.linalg.det(cell) for cell in self.outcar.parse_dict["cells"]] ) self.generic_output.dft_log_dict["scf_energy_free"] = ( self.outcar.parse_dict["scf_energies"] ) self.generic_output.dft_log_dict["scf_dipole_mom"] = self.outcar.parse_dict[ "scf_dipole_moments" ] self.generic_output.dft_log_dict["n_elect"] = self.outcar.parse_dict[ "n_elect" ] self.generic_output.dft_log_dict["energy_int"] = self.outcar.parse_dict[ "energies_int" ] self.generic_output.dft_log_dict["energy_free"] = self.outcar.parse_dict[ "energies" ] self.generic_output.dft_log_dict["energy_zero"] = self.outcar.parse_dict[ "energies_zero" ] self.generic_output.dft_log_dict["energy_int"] = self.outcar.parse_dict[ "energies_int" ] if "PROCAR" in files_present: try: self.electronic_structure = self.procar.from_file( filename=posixpath.join(directory, "PROCAR") ) # Even the atom resolved values have to be sorted from the vasp atoms order to the Atoms order self.electronic_structure.grand_dos_matrix[ :, :, :, sorted_indices, : ] = self.electronic_structure.grand_dos_matrix[:, :, :, :, :].copy() try: self.electronic_structure.efermi = self.outcar.parse_dict[ "fermi_level" ] except KeyError: self.electronic_structure.efermi = self.vp_new.vasprun_dict[ "efermi" ] except ValueError: pass # important that we "reverse sort" the atoms in the vasp format into the atoms in the atoms class self.generic_output.log_dict = log_dict if vasprun_working: # self.dft_output.log_dict["parameters"] = self.vp_new.vasprun_dict["parameters"] self.generic_output.dft_log_dict["scf_dipole_mom"] = ( self.vp_new.vasprun_dict["scf_dipole_moments"] ) if len(self.generic_output.dft_log_dict["scf_dipole_mom"][0]) > 0: total_dipole_moments = np.array( [ dip[-1] for dip in self.generic_output.dft_log_dict["scf_dipole_mom"] ] ) self.generic_output.dft_log_dict["dipole_mom"] = total_dipole_moments self.generic_output.dft_log_dict["scf_energy_int"] = ( self.vp_new.vasprun_dict["scf_energies"] ) self.generic_output.dft_log_dict["scf_energy_free"] = ( self.vp_new.vasprun_dict["scf_fr_energies"] ) self.generic_output.dft_log_dict["scf_energy_zero"] = ( self.vp_new.vasprun_dict["scf_0_energies"] ) self.generic_output.dft_log_dict["energy_int"] = np.array( [ e_int[-1] for e_int in self.generic_output.dft_log_dict["scf_energy_int"] ] ) self.generic_output.dft_log_dict["energy_free"] = np.array( [ e_free[-1] for e_free in self.generic_output.dft_log_dict["scf_energy_free"] ] ) # Overwrite energy_free with much better precision from the OSZICAR file if "energy_pot" in self.oszicar.parse_dict and np.array_equal( self.generic_output.dft_log_dict["energy_free"], np.round(self.oszicar.parse_dict["energy_pot"], 8), ): self.generic_output.dft_log_dict["energy_free"] = ( self.oszicar.parse_dict["energy_pot"] ) self.generic_output.dft_log_dict["energy_zero"] = np.array( [ e_zero[-1] for e_zero in self.generic_output.dft_log_dict["scf_energy_zero"] ] ) self.generic_output.dft_log_dict["n_elect"] = float( self.vp_new.vasprun_dict["parameters"]["electronic"]["NELECT"] ) if "kinetic_energies" in self.vp_new.vasprun_dict: # scf_energy_kin is for backwards compatibility self.generic_output.dft_log_dict["scf_energy_kin"] = ( self.vp_new.vasprun_dict["kinetic_energies"] ) self.generic_output.dft_log_dict["energy_kin"] = ( self.vp_new.vasprun_dict["kinetic_energies"] ) if ( "LOCPOT" in files_present and os.stat(posixpath.join(directory, "LOCPOT")).st_size != 0 ): self.electrostatic_potential.from_file( filename=posixpath.join(directory, "LOCPOT"), normalize=False ) if ( "CHGCAR" in files_present and os.stat(posixpath.join(directory, "CHGCAR")).st_size != 0 ): self.charge_density.from_file( filename=posixpath.join(directory, "CHGCAR"), normalize=True ) self.generic_output.bands = self.electronic_structure
def to_dict(self): output_dict = { "description": self.description, "generic": self.generic_output.to_dict(), } if self.structure is not None and type(self.structure) == Atoms: output_dict["structure"] = self.structure.todict() elif self.structure is not None: output_dict["structure"] = self.structure.to_dict() if self.electrostatic_potential.total_data is not None: output_dict["electrostatic_potential"] = ( self.electrostatic_potential.to_dict() ) if self.charge_density.total_data is not None: output_dict["charge_density"] = self.charge_density.to_dict() if len(self.electronic_structure.kpoint_list) > 0: output_dict["electronic_structure"] = self.electronic_structure.to_dict() if len(self.outcar.parse_dict.keys()) > 0: output_dict["outcar"] = self.outcar.to_dict_minimal() return output_dict class GenericOutput: """ This class stores the generic output like different structures, energies and forces from a simulation in a highly generic format. Usually the user does not have to access this class. Attributes: log_dict (dict): A dictionary of all tags and values of generic data (positions, forces, etc) """ def __init__(self): self.log_dict = {} self.dft_log_dict = {} self.description = "generic_output contains generic output static" self._bands = ElectronicStructure() @property def bands(self): return self._bands @bands.setter def bands(self, val): self._bands = val def to_dict(self): go_dict, dft_dict = {}, {} for key, val in self.log_dict.items(): go_dict[key] = val for key, val in self.dft_log_dict.items(): dft_dict[key] = val go_dict["dft"] = dft_dict if self.bands.eigenvalue_matrix is not None: go_dict["dft"]["bands"] = self.bands.to_dict() return go_dict class VaspCollectError(ValueError): pass def parse_vasp_output( working_directory: str, structure: Optional[Atoms] = None, sorted_indices: Optional[Union[Sequence[int], np.ndarray]] = None, read_atoms_funct: Callable = read_atoms, es_class=ElectronicStructure, bader_class=Bader, output_parser_class=Output, ) -> dict: """ Parse the VASP output in the working_directory and return it as hierachical dictionary. Args: working_directory (str): directory of the VASP calculation structure (Atoms): atomistic structure as optional input for matching the output to the input of the calculation sorted_indices (list): list of indices used to sort the atomistic structure Returns: dict: hierarchical output dictionary """ output_parser = output_parser_class() if structure is None or len(structure) == 0: try: structure = get_final_structure_from_file( working_directory=working_directory, filename="CONTCAR", read_atoms_funct=read_atoms_funct, ) except OSError: structure = get_final_structure_from_file( working_directory=working_directory, filename="POSCAR", read_atoms_funct=read_atoms_funct, ) if sorted_indices is None: sorted_indices_array = np.arange(len(structure)) else: sorted_indices_array = np.asarray(sorted_indices) output_parser.structure = structure.copy() try: output_parser.collect( directory=working_directory, sorted_indices=sorted_indices_array, es_class=es_class, ) except VaspCollectError: raise # Try getting high precision positions from CONTCAR with contextlib.suppress(IOError, ValueError, FileNotFoundError): output_parser.structure = get_final_structure_from_file( working_directory=working_directory, filename="CONTCAR", structure=structure, sorted_indices=sorted_indices_array, read_atoms_funct=read_atoms_funct, ) # Bader analysis if os.path.isfile(os.path.join(working_directory, "AECCAR0")) and os.path.isfile( os.path.join(working_directory, "AECCAR2") ): bader = bader_class(working_directory=working_directory, structure=structure) try: charges_orig, volumes_orig = bader.compute_bader_charges() except ValueError: warnings.warn("Invoking Bader charge analysis failed", stacklevel=2) else: charges, volumes = charges_orig.copy(), volumes_orig.copy() charges[sorted_indices_array] = charges_orig volumes[sorted_indices_array] = volumes_orig if "valence_charges" in output_parser.generic_output.dft_log_dict: valence_charges = output_parser.generic_output.dft_log_dict[ "valence_charges" ] # Positive values indicate electron depletion output_parser.generic_output.dft_log_dict["bader_charges"] = ( valence_charges - charges ) output_parser.generic_output.dft_log_dict["bader_volumes"] = volumes return output_parser.to_dict() def get_final_structure_from_file( working_directory: str, filename: str = "CONTCAR", structure: Optional[Atoms] = None, sorted_indices: Optional[Union[Sequence[int], np.ndarray]] = None, read_atoms_funct: Callable = read_atoms, ) -> Atoms: """ Get the final structure of the simulation usually from the CONTCAR file Args: filename (str): Path to the CONTCAR file in VASP Returns: pyiron.atomistics.structure.atoms.Atoms: The final structure """ filename = posixpath.join(working_directory, filename) if structure is not None and sorted_indices is None: sorted_indices_array = vasp_sorter(structure) elif sorted_indices is not None: sorted_indices_array = np.asarray(sorted_indices) else: sorted_indices_array = None if structure is None: try: output_structure = read_atoms_funct(filename=filename) input_structure = output_structure.copy() except (OSError, IndexError, ValueError): raise OSError("Unable to read output structure") else: input_structure = structure.copy() if type(input_structure) == Atoms: species_list = input_structure.get_chemical_symbols() else: species_list = input_structure.get_parent_symbols() try: output_structure = read_atoms_funct( filename=filename, species_list=species_list, ) input_structure.cell = output_structure.cell.copy() input_structure.positions[sorted_indices_array] = output_structure.positions except (OSError, IndexError, ValueError): raise OSError("Unable to read output structure") return input_structure