From 02f6ce583934d564cb154cb700aa055724719e2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:27:33 +0000 Subject: [PATCH 1/8] Bump phonopy from 2.23.1 to 2.24.2 Bumps [phonopy](https://phonopy.github.io/phonopy/) from 2.23.1 to 2.24.2. --- updated-dependencies: - dependency-name: phonopy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9a0ef2ca4..d2aa1a33c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "mp-api==0.41.2", "numpy==1.26.4", "pandas==2.2.2", - "phonopy==2.23.1", + "phonopy==2.24.2", "pint==0.23", "pyiron_base==0.9.1", "pyiron_snippets==0.1.1", From 736262450b7323fcec9bcf553d7e7a8c3569e474 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 08:31:57 +0000 Subject: [PATCH 2/8] [dependabot skip] Update environment --- .ci_support/environment-docs.yml | 2 +- .ci_support/environment.yml | 2 +- binder/environment.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.ci_support/environment-docs.yml b/.ci_support/environment-docs.yml index 3dd054fdb..460807e08 100644 --- a/.ci_support/environment-docs.yml +++ b/.ci_support/environment-docs.yml @@ -15,7 +15,7 @@ dependencies: - mp-api =0.41.2 - numpy =1.26.4 - pandas =2.2.2 -- phonopy =2.23.1 +- phonopy =2.24.2 - pint =0.23 - pyiron_base =0.9.1 - pyiron_snippets =0.1.1 diff --git a/.ci_support/environment.yml b/.ci_support/environment.yml index 619b0592d..c7b42ba21 100644 --- a/.ci_support/environment.yml +++ b/.ci_support/environment.yml @@ -13,7 +13,7 @@ dependencies: - mp-api =0.41.2 - numpy =1.26.4 - pandas =2.2.2 -- phonopy =2.23.1 +- phonopy =2.24.2 - pint =0.23 - pyiron_base =0.9.1 - pyiron_snippets =0.1.1 diff --git a/binder/environment.yml b/binder/environment.yml index 23bf2593f..46baa1bd8 100644 --- a/binder/environment.yml +++ b/binder/environment.yml @@ -11,7 +11,7 @@ dependencies: - mp-api =0.41.2 - numpy =1.26.4 - pandas =2.2.2 -- phonopy =2.23.1 +- phonopy =2.24.2 - pint =0.23 - pyiron_base =0.9.1 - pylammpsmpi =0.2.19 From e20d94a7da47b8619731997c4dfbaef6ba4f8f59 Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Mon, 10 Jun 2024 14:31:17 +0200 Subject: [PATCH 3/8] VASP: Encapsulate the output parser in the parse_vasp_output() function --- pyiron_atomistics/dft/bader.py | 31 +- pyiron_atomistics/vasp/base.py | 723 ++------------------------ pyiron_atomistics/vasp/interactive.py | 5 +- pyiron_atomistics/vasp/output.py | 699 +++++++++++++++++++++++++ 4 files changed, 760 insertions(+), 698 deletions(-) create mode 100644 pyiron_atomistics/vasp/output.py diff --git a/pyiron_atomistics/dft/bader.py b/pyiron_atomistics/dft/bader.py index c7260f8fd..6a1b17929 100644 --- a/pyiron_atomistics/dft/bader.py +++ b/pyiron_atomistics/dft/bader.py @@ -6,6 +6,9 @@ import os import subprocess +from pyiron_atomistics.vasp.volumetric_data import VaspVolumetricData + + __author__ = "Sudarsan Surendralal" __copyright__ = ( "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - " @@ -26,22 +29,21 @@ class Bader: .. _Bader code: http://theory.cm.utexas.edu/henkelman/code/bader """ - def __init__(self, job): + def __init__(self, structure, working_directory): """ Initialize the Bader module Args: job (pyiron_atomistics.dft.job.generic.GenericDFTJob): A DFT job instance (finished/converged job) """ - self.job = job - self._working_directory = job.working_directory - self._structure = job.structure + self._working_directory = working_directory + self._structure = structure def _create_cube_files(self): """ Create CUBE format files of the total and valce charges to be used by the Bader program """ - cd_val, cd_total = self.job.get_valence_and_total_charge_density() + cd_val, cd_total = get_valence_and_total_charge_density(working_directory=self._working_directory) cd_val.write_cube_file( filename=os.path.join(self._working_directory, "valence_charge.CUBE") ) @@ -124,3 +126,22 @@ def parse_charge_vol_file(structure, filename="ACF.dat"): charges = np.genfromtxt(lines[2:], max_rows=len(structure))[:, 4] volumes = np.genfromtxt(lines[2:], max_rows=len(structure))[:, 6] return charges, volumes + + +def get_valence_and_total_charge_density(working_directory): + """ + Gives the valence and total charge densities + + Returns: + tuple: The required charge densities + """ + cd_core = VaspVolumetricData() + cd_total = VaspVolumetricData() + cd_val = VaspVolumetricData() + if os.path.isfile(working_directory + "/AECCAR0"): + cd_core.from_file(working_directory + "/AECCAR0") + cd_val.from_file(working_directory + "/AECCAR2") + cd_val.atoms = cd_val.atoms + cd_total.total_data = cd_core.total_data + cd_val.total_data + cd_total.atoms = cd_val.atoms + return cd_val, cd_total diff --git a/pyiron_atomistics/vasp/base.py b/pyiron_atomistics/vasp/base.py index cc42ac7ea..d989e13da 100644 --- a/pyiron_atomistics/vasp/base.py +++ b/pyiron_atomistics/vasp/base.py @@ -16,31 +16,23 @@ Potcar, strip_xc_from_potential_name, ) -from pyiron_atomistics.atomistics.structure.atoms import ( - Atoms, - CrystalStructure, - structure_dict_to_hdf, - dict_group_to_hdf, -) +from pyiron_atomistics.atomistics.structure.atoms import CrystalStructure from pyiron_base import state, GenericParameters from pyiron_snippets.deprecate import deprecate -from pyiron_atomistics.vasp.parser.outcar import Outcar, OutcarCollectError -from pyiron_atomistics.vasp.parser.oszicar import Oszicar -from pyiron_atomistics.vasp.procar import Procar +from pyiron_atomistics.vasp.output import ( + VaspCollectError, + Output, + get_final_structure_from_file, + output_dict_to_hdf, + parse_vasp_output, +) from pyiron_atomistics.vasp.structure import read_atoms, write_poscar, vasp_sorter from pyiron_atomistics.vasp.vasprun import Vasprun as Vr -from pyiron_atomistics.vasp.vasprun import VasprunError, VasprunWarning -from pyiron_atomistics.vasp.volumetric_data import ( - VaspVolumetricData, - volumetric_data_dict_to_hdf, -) +from pyiron_atomistics.vasp.vasprun import VasprunError +from pyiron_atomistics.vasp.volumetric_data import VaspVolumetricData from pyiron_atomistics.vasp.potential import get_enmax_among_potentials -from pyiron_atomistics.dft.waves.electronic import ( - ElectronicStructure, - electronic_structure_dict_to_hdf, -) from pyiron_atomistics.dft.waves.bandstructure import Bandstructure -from pyiron_atomistics.dft.bader import Bader +from pyiron_atomistics.dft.bader import get_valence_and_total_charge_density import warnings __author__ = "Sudarsan Surendralal, Felix Lochner" @@ -400,79 +392,27 @@ def write_input(self): modified_elements=modified_elements, ) - def collect_output_parser(self, cwd): - """ - Collects the outputs and stores them to the hdf file - """ - if self.structure is None or len(self.structure) == 0: - try: - self.structure = self.get_final_structure_from_file( - cwd=cwd, filename="CONTCAR" - ) - except IOError: - self.structure = self.get_final_structure_from_file( - cwd=cwd, filename="POSCAR" - ) - self._sorted_indices = np.array(range(len(self.structure))) - self._output_parser.structure = self.structure.copy() - try: - self._output_parser.collect( - directory=cwd, sorted_indices=self.sorted_indices - ) - except VaspCollectError: - self.status.aborted = True - raise - # Try getting high precision positions from CONTCAR - try: - self._output_parser.structure = self.get_final_structure_from_file( - cwd=cwd, - filename="CONTCAR", - ) - except (IOError, ValueError, FileNotFoundError): - pass - - # Bader analysis - if os.path.isfile(os.path.join(cwd, "AECCAR0")) and os.path.isfile( - os.path.join(cwd, "AECCAR2") - ): - bader = Bader(self) - try: - charges_orig, volumes_orig = bader.compute_bader_charges() - except ValueError: - warnings.warn("Invoking Bader charge analysis failed") - self.logger.warning("Invoking Bader charge analysis failed") - else: - charges, volumes = charges_orig.copy(), volumes_orig.copy() - charges[self.sorted_indices] = charges_orig - volumes[self.sorted_indices] = volumes_orig - if ( - "valence_charges" - in self._output_parser.generic_output.dft_log_dict.keys() - ): - valence_charges = self._output_parser.generic_output.dft_log_dict[ - "valence_charges" - ] - # Positive values indicate electron depletion - self._output_parser.generic_output.dft_log_dict["bader_charges"] = ( - valence_charges - charges - ) - self._output_parser.generic_output.dft_log_dict["bader_volumes"] = ( - volumes - ) - return self._output_parser.to_dict() + def _store_output(self, output_dict): + output_dict_to_hdf( + data_dict=output_dict, + hdf=self._hdf5, + group_name="output", + ) + if len(self._exclude_groups_hdf) > 0 or len(self._exclude_nodes_hdf) > 0: + self.project_hdf5.rewrite_hdf5() # define routines that collect all output files def collect_output(self): """ Collects the outputs and stores them to the hdf file """ - output_dict_to_hdf( - data_dict=self.collect_output_parser(cwd=self.working_directory), - hdf=self._hdf5, - group_name="output", + self._store_output( + output_dict=parse_vasp_output( + working_directory=self.working_directory, + structure=self.structure, + sorted_indices=self.sorted_indices, + ), ) - if len(self._exclude_groups_hdf) > 0 or len(self._exclude_nodes_hdf) > 0: - self.project_hdf5.rewrite_hdf5() def convergence_check(self): """ @@ -833,27 +773,12 @@ def get_final_structure_from_file(self, cwd, filename="CONTCAR"): Returns: pyiron.atomistics.structure.atoms.Atoms: The final structure """ - filename = posixpath.join(cwd, filename) - if self.structure is None: - try: - output_structure = read_atoms(filename=filename) - input_structure = output_structure.copy() - except (IndexError, ValueError, IOError): - raise IOError("Unable to read output structure") - else: - input_structure = self.structure.copy() - try: - output_structure = read_atoms( - filename=filename, - species_list=input_structure.get_parent_symbols(), - ) - input_structure.cell = output_structure.cell.copy() - input_structure.positions[self.sorted_indices] = ( - output_structure.positions - ) - except (IndexError, ValueError, IOError): - raise IOError("Unable to read output structure") - return input_structure + return get_final_structure_from_file( + working_directory=cwd, + filename=filename, + structure=self.structure, + sorted_indices=self.sorted_indices, + ) def write_magmoms(self): """ @@ -1460,16 +1385,9 @@ def get_valence_and_total_charge_density(self): Returns: tuple: The required charge densities """ - cd_core = VaspVolumetricData() - cd_total = VaspVolumetricData() - cd_val = VaspVolumetricData() - if os.path.isfile(self.working_directory + "/AECCAR0"): - cd_core.from_file(self.working_directory + "/AECCAR0") - cd_val.from_file(self.working_directory + "/AECCAR2") - cd_val.atoms = cd_val.atoms - cd_total.total_data = cd_core.total_data + cd_val.total_data - cd_total.atoms = cd_val.atoms - return cd_val, cd_total + return get_valence_and_total_charge_density( + working_directory=self.working_directory + ) def get_electrostatic_potential(self): """ @@ -2012,505 +1930,6 @@ def _eddrmm_backwards_compatibility(eddrmm_value): return eddrmm_value -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.dft_output = DFTOutput() - 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): - """ - Setter for the output structure - """ - self._structure = atoms - - def collect(self, directory=os.getcwd(), sorted_indices=None): - """ - 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 = dict() - vasprun_working, outcar_working = False, False - if not ("OUTCAR" in files_present or "vasprun.xml" in files_present): - raise IOError("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: - state.logger.warning(f"OUTCAR present, but could not be parsed: {e}!") - 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]): - state.logger.warning( - "vasprun.xml parsed but with some inconsistencies. " - "Check vasp output to be sure" - ) - warnings.warn( - "vasprun.xml parsed but with some inconsistencies. " - "Check vasp output to be sure", - VasprunWarning, - ) - except VasprunError: - state.logger.warning( - "Unable to parse the vasprun.xml file. Will attempt to get data from OUTCAR" - ) - 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.keys(): - 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() - 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: - raise VaspCollectError("Improper OUTCAR parsing") - elif log_dict["positions"].shape[1] != len(sorted_indices): - raise VaspCollectError("Improper OUTCAR parsing") - if len(log_dict["forces"].shape) != 3: - raise VaspCollectError("Improper OUTCAR parsing") - elif 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.keys(): - if 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.keys(): - # 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): - hdf5_output = { - "description": self.description, - "generic": self.generic_output.to_dict(), - } - - if self._structure is not None: - hdf5_output["structure"] = self.structure.to_dict() - - if self.electrostatic_potential.total_data is not None: - hdf5_output["electrostatic_potential"] = ( - self.electrostatic_potential.to_dict() - ) - - if self.charge_density.total_data is not None: - hdf5_output["charge_density"] = self.charge_density.to_dict() - - if len(self.electronic_structure.kpoint_list) > 0: - hdf5_output["electronic_structure"] = self.electronic_structure.to_dict() - - if len(self.outcar.parse_dict.keys()) > 0: - hdf5_output["outcar"] = self.outcar.to_dict_minimal() - return hdf5_output - - def to_hdf(self, hdf): - """ - Save the object in a HDF5 file - - Args: - hdf (pyiron_base.generic.hdfio.ProjectHDFio): HDF path to which the object is to be saved - - """ - output_dict_to_hdf(data_dict=self.to_dict(), hdf=hdf, group_name="output") - - def from_hdf(self, hdf): - """ - Reads the attributes and reconstructs the object from a hdf file - Args: - hdf: The hdf5 instance - """ - with hdf.open("output") as hdf5_output: - # self.description = hdf5_output["description"] - if self.structure is None: - self.structure = Atoms() - self.structure.from_hdf(hdf5_output) - self.generic_output.from_hdf(hdf5_output) - try: - if "electrostatic_potential" in hdf5_output.list_groups(): - self.electrostatic_potential.from_hdf( - hdf5_output, group_name="electrostatic_potential" - ) - if "charge_density" in hdf5_output.list_groups(): - self.charge_density.from_hdf( - hdf5_output, group_name="charge_density" - ) - if "electronic_structure" in hdf5_output.list_groups(): - self.electronic_structure.from_hdf(hdf=hdf5_output) - if "outcar" in hdf5_output.list_groups(): - self.outcar.from_hdf(hdf=hdf5_output, group_name="outcar") - except (TypeError, IOError, ValueError): - state.logger.warning("Routine from_hdf() not completely successful") - - -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 = dict() - self.dft_log_dict = 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_hdf(self, hdf): - """ - Save the object in a HDF5 file - - Args: - hdf (pyiron_base.generic.hdfio.ProjectHDFio): HDF path to which the object is to be saved - - """ - generic_output_dict_to_hdf( - data_dict=self.to_dict(), hdf=hdf, group_name="generic" - ) - - def to_dict(self): - hdf_go, hdf_dft = {}, {} - for key, val in self.log_dict.items(): - hdf_go[key] = val - for key, val in self.dft_log_dict.items(): - hdf_dft[key] = val - hdf_go["dft"] = hdf_dft - if self.bands.eigenvalue_matrix is not None: - hdf_go["dft"]["bands"] = self.bands.to_dict() - return hdf_go - - def from_hdf(self, hdf): - """ - Reads the attributes and reconstructs the object from a hdf file - Args: - hdf: The hdf5 instance - """ - with hdf.open("generic") as hdf_go: - for node in hdf_go.list_nodes(): - if node == "description": - # self.description = hdf_go[node] - pass - else: - self.log_dict[node] = hdf_go[node] - if "dft" in hdf_go.list_groups(): - with hdf_go.open("dft") as hdf_dft: - for node in hdf_dft.list_nodes(): - self.dft_log_dict[node] = hdf_dft[node] - if "bands" in hdf_dft.list_groups(): - self.bands.from_hdf(hdf_dft, "bands") - - -class DFTOutput: - """ - This class stores the DFT specific output - - Attributes: - log_dict (dict): A dictionary of all tags and values of DFT data - """ - - def __init__(self): - self.log_dict = dict() - self.description = "contains DFT specific output" - - def to_hdf(self, hdf): - """ - Save the object in a HDF5 file - - Args: - hdf (pyiron_base.generic.hdfio.ProjectHDFio): HDF path to which the object is to be saved - - """ - with hdf.open("dft") as hdf_dft: - # hdf_go["description"] = self.description - for key, val in self.log_dict.items(): - hdf_dft[key] = val - - def from_hdf(self, hdf): - """ - Reads the attributes and reconstructs the object from a hdf file - Args: - hdf: The hdf5 instance - """ - with hdf.open("dft") as hdf_dft: - for node in hdf_dft.list_nodes(): - if node == "description": - # self.description = hdf_go[node] - pass - else: - self.log_dict[node] = hdf_dft[node] - - class Incar(GenericParameters): """ Class to control the INCAR file of a vasp simulation @@ -2666,77 +2085,3 @@ def get_k_mesh_by_cell(cell, kspace_per_in_ang=0.10): kmesh = np.ceil(np.array([2 * np.pi / ll for ll in latlens]) / kspace_per_in_ang) kmesh[kmesh < 1] = 1 return kmesh - - -class VaspCollectError(ValueError): - pass - - -def generic_output_dict_to_hdf(data_dict, hdf, group_name="generic"): - with hdf.open(group_name) as hdf_go: - for k, v in data_dict.items(): - if k not in ["dft"]: - hdf_go[k] = v - - with hdf_go.open("dft") as hdf_dft: - for k, v in data_dict["dft"].items(): - if k not in ["bands"]: - hdf_dft[k] = v - - if "bands" in data_dict["dft"].keys(): - electronic_structure_dict_to_hdf( - data_dict=data_dict["dft"]["bands"], - hdf=hdf_dft, - group_name="bands", - ) - - -def output_dict_to_hdf(data_dict, hdf, group_name="output"): - with hdf.open(group_name) as hdf5_output: - for k, v in data_dict.items(): - if k not in [ - "generic", - "structure", - "electrostatic_potential", - "charge_density", - "electronic_structure", - "outcar", - ]: - hdf5_output[k] = v - - if "generic" in data_dict.keys(): - generic_output_dict_to_hdf( - data_dict=data_dict["generic"], - hdf=hdf5_output, - group_name="generic", - ) - - if "structure" in data_dict.keys(): - structure_dict_to_hdf( - data_dict=data_dict["structure"], - hdf=hdf5_output, - group_name="structure", - ) - - if "electrostatic_potential" in data_dict.keys(): - volumetric_data_dict_to_hdf( - data_dict=data_dict["electrostatic_potential"], - hdf=hdf5_output, - group_name="electrostatic_potential", - ) - - if "charge_density" in data_dict.keys(): - volumetric_data_dict_to_hdf( - data_dict=data_dict["charge_density"], - hdf=hdf5_output, - group_name="charge_density", - ) - - if "electronic_structure" in data_dict.keys(): - electronic_structure_dict_to_hdf( - data_dict=data_dict["electronic_structure"], - hdf=hdf5_output, - group_name="electronic_structure", - ) - - dict_group_to_hdf(data_dict=data_dict, hdf=hdf5_output, group="outcar") diff --git a/pyiron_atomistics/vasp/interactive.py b/pyiron_atomistics/vasp/interactive.py index c2b0f0109..8e60795e8 100644 --- a/pyiron_atomistics/vasp/interactive.py +++ b/pyiron_atomistics/vasp/interactive.py @@ -9,14 +9,11 @@ from pyiron_atomistics.vasp.parser.outcar import Outcar from pyiron_atomistics.vasp.base import VaspBase from pyiron_atomistics.vasp.structure import vasp_sorter -from pyiron_atomistics.vasp.potential import VaspPotentialSetter from pyiron_atomistics.atomistics.job.interactive import GenericInteractive # as of pyiron_atomistics <= 0.5.4 this module defined subclasses that are now removed; the base classes are still # imported here in case HDF5 files in the wild refer to them. The imports can be removed on the next big version bump. -from pyiron_atomistics.vasp.base import GenericOutput -from pyiron_atomistics.vasp.base import DFTOutput -from pyiron_atomistics.vasp.base import Output +from pyiron_atomistics.vasp.output import Output, DFTOutput, GenericOutput __author__ = "Osamu Waseda, Jan Janssen" __copyright__ = ( diff --git a/pyiron_atomistics/vasp/output.py b/pyiron_atomistics/vasp/output.py new file mode 100644 index 000000000..5094f39b2 --- /dev/null +++ b/pyiron_atomistics/vasp/output.py @@ -0,0 +1,699 @@ +from __future__ import print_function +import os +import posixpath +import numpy as np + +from pyiron_atomistics.atomistics.structure.atoms import ( + Atoms, + structure_dict_to_hdf, + dict_group_to_hdf, +) +from pyiron_base import state +from pyiron_atomistics.vasp.parser.outcar import Outcar, OutcarCollectError +from pyiron_atomistics.vasp.parser.oszicar import Oszicar +from pyiron_atomistics.vasp.procar import Procar +from pyiron_atomistics.vasp.structure import read_atoms, vasp_sorter +from pyiron_atomistics.vasp.vasprun import Vasprun as Vr +from pyiron_atomistics.vasp.vasprun import VasprunError, VasprunWarning +from pyiron_atomistics.vasp.volumetric_data import ( + VaspVolumetricData, + volumetric_data_dict_to_hdf, +) +from pyiron_atomistics.dft.bader import Bader +from pyiron_atomistics.dft.waves.electronic import ( + ElectronicStructure, + electronic_structure_dict_to_hdf, +) +import warnings + + +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.dft_output = DFTOutput() + 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): + """ + Setter for the output structure + """ + self._structure = atoms + + def collect(self, directory=os.getcwd(), sorted_indices=None): + """ + 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 = dict() + vasprun_working, outcar_working = False, False + if not ("OUTCAR" in files_present or "vasprun.xml" in files_present): + raise IOError("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: + state.logger.warning(f"OUTCAR present, but could not be parsed: {e}!") + 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]): + state.logger.warning( + "vasprun.xml parsed but with some inconsistencies. " + "Check vasp output to be sure" + ) + warnings.warn( + "vasprun.xml parsed but with some inconsistencies. " + "Check vasp output to be sure", + VasprunWarning, + ) + except VasprunError: + state.logger.warning( + "Unable to parse the vasprun.xml file. Will attempt to get data from OUTCAR" + ) + 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.keys(): + 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() + 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: + raise VaspCollectError("Improper OUTCAR parsing") + elif log_dict["positions"].shape[1] != len(sorted_indices): + raise VaspCollectError("Improper OUTCAR parsing") + if len(log_dict["forces"].shape) != 3: + raise VaspCollectError("Improper OUTCAR parsing") + elif 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.keys(): + if 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.keys(): + # 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): + hdf5_output = { + "description": self.description, + "generic": self.generic_output.to_dict(), + } + + if self._structure is not None: + hdf5_output["structure"] = self.structure.to_dict() + + if self.electrostatic_potential.total_data is not None: + hdf5_output["electrostatic_potential"] = ( + self.electrostatic_potential.to_dict() + ) + + if self.charge_density.total_data is not None: + hdf5_output["charge_density"] = self.charge_density.to_dict() + + if len(self.electronic_structure.kpoint_list) > 0: + hdf5_output["electronic_structure"] = self.electronic_structure.to_dict() + + if len(self.outcar.parse_dict.keys()) > 0: + hdf5_output["outcar"] = self.outcar.to_dict_minimal() + return hdf5_output + + def to_hdf(self, hdf): + """ + Save the object in a HDF5 file + + Args: + hdf (pyiron_base.generic.hdfio.ProjectHDFio): HDF path to which the object is to be saved + + """ + output_dict_to_hdf(data_dict=self.to_dict(), hdf=hdf, group_name="output") + + def from_hdf(self, hdf): + """ + Reads the attributes and reconstructs the object from a hdf file + Args: + hdf: The hdf5 instance + """ + with hdf.open("output") as hdf5_output: + # self.description = hdf5_output["description"] + if self.structure is None: + self.structure = Atoms() + self.structure.from_hdf(hdf5_output) + self.generic_output.from_hdf(hdf5_output) + try: + if "electrostatic_potential" in hdf5_output.list_groups(): + self.electrostatic_potential.from_hdf( + hdf5_output, group_name="electrostatic_potential" + ) + if "charge_density" in hdf5_output.list_groups(): + self.charge_density.from_hdf( + hdf5_output, group_name="charge_density" + ) + if "electronic_structure" in hdf5_output.list_groups(): + self.electronic_structure.from_hdf(hdf=hdf5_output) + if "outcar" in hdf5_output.list_groups(): + self.outcar.from_hdf(hdf=hdf5_output, group_name="outcar") + except (TypeError, IOError, ValueError): + state.logger.warning("Routine from_hdf() not completely successful") + + +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 = dict() + self.dft_log_dict = 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_hdf(self, hdf): + """ + Save the object in a HDF5 file + + Args: + hdf (pyiron_base.generic.hdfio.ProjectHDFio): HDF path to which the object is to be saved + + """ + generic_output_dict_to_hdf( + data_dict=self.to_dict(), hdf=hdf, group_name="generic" + ) + + def to_dict(self): + hdf_go, hdf_dft = {}, {} + for key, val in self.log_dict.items(): + hdf_go[key] = val + for key, val in self.dft_log_dict.items(): + hdf_dft[key] = val + hdf_go["dft"] = hdf_dft + if self.bands.eigenvalue_matrix is not None: + hdf_go["dft"]["bands"] = self.bands.to_dict() + return hdf_go + + def from_hdf(self, hdf): + """ + Reads the attributes and reconstructs the object from a hdf file + Args: + hdf: The hdf5 instance + """ + with hdf.open("generic") as hdf_go: + for node in hdf_go.list_nodes(): + if node == "description": + # self.description = hdf_go[node] + pass + else: + self.log_dict[node] = hdf_go[node] + if "dft" in hdf_go.list_groups(): + with hdf_go.open("dft") as hdf_dft: + for node in hdf_dft.list_nodes(): + self.dft_log_dict[node] = hdf_dft[node] + if "bands" in hdf_dft.list_groups(): + self.bands.from_hdf(hdf_dft, "bands") + + +class DFTOutput: + """ + This class stores the DFT specific output + + Attributes: + log_dict (dict): A dictionary of all tags and values of DFT data + """ + + def __init__(self): + self.log_dict = dict() + self.description = "contains DFT specific output" + + def to_hdf(self, hdf): + """ + Save the object in a HDF5 file + + Args: + hdf (pyiron_base.generic.hdfio.ProjectHDFio): HDF path to which the object is to be saved + + """ + with hdf.open("dft") as hdf_dft: + # hdf_go["description"] = self.description + for key, val in self.log_dict.items(): + hdf_dft[key] = val + + def from_hdf(self, hdf): + """ + Reads the attributes and reconstructs the object from a hdf file + Args: + hdf: The hdf5 instance + """ + with hdf.open("dft") as hdf_dft: + for node in hdf_dft.list_nodes(): + if node == "description": + # self.description = hdf_go[node] + pass + else: + self.log_dict[node] = hdf_dft[node] + + +class VaspCollectError(ValueError): + pass + + +def generic_output_dict_to_hdf(data_dict, hdf, group_name="generic"): + with hdf.open(group_name) as hdf_go: + for k, v in data_dict.items(): + if k not in ["dft"]: + hdf_go[k] = v + + with hdf_go.open("dft") as hdf_dft: + for k, v in data_dict["dft"].items(): + if k not in ["bands"]: + hdf_dft[k] = v + + if "bands" in data_dict["dft"].keys(): + electronic_structure_dict_to_hdf( + data_dict=data_dict["dft"]["bands"], + hdf=hdf_dft, + group_name="bands", + ) + + +def output_dict_to_hdf(data_dict, hdf, group_name="output"): + with hdf.open(group_name) as hdf5_output: + for k, v in data_dict.items(): + if k not in [ + "generic", + "structure", + "electrostatic_potential", + "charge_density", + "electronic_structure", + "outcar", + ]: + hdf5_output[k] = v + + if "generic" in data_dict.keys(): + generic_output_dict_to_hdf( + data_dict=data_dict["generic"], + hdf=hdf5_output, + group_name="generic", + ) + + if "structure" in data_dict.keys(): + structure_dict_to_hdf( + data_dict=data_dict["structure"], + hdf=hdf5_output, + group_name="structure", + ) + + if "electrostatic_potential" in data_dict.keys(): + volumetric_data_dict_to_hdf( + data_dict=data_dict["electrostatic_potential"], + hdf=hdf5_output, + group_name="electrostatic_potential", + ) + + if "charge_density" in data_dict.keys(): + volumetric_data_dict_to_hdf( + data_dict=data_dict["charge_density"], + hdf=hdf5_output, + group_name="charge_density", + ) + + if "electronic_structure" in data_dict.keys(): + electronic_structure_dict_to_hdf( + data_dict=data_dict["electronic_structure"], + hdf=hdf5_output, + group_name="electronic_structure", + ) + + dict_group_to_hdf(data_dict=data_dict, hdf=hdf5_output, group="outcar") + + +def parse_vasp_output(working_directory, structure=None, sorted_indices=None): + """ + Collects the outputs and stores them to the hdf file + """ + output_parser = Output() + if structure is None or len(structure) == 0: + try: + structure = get_final_structure_from_file( + working_directory=working_directory, filename="CONTCAR" + ) + except IOError: + structure = get_final_structure_from_file( + working_directory=working_directory, filename="POSCAR" + ) + if sorted_indices is None: + sorted_indices = np.array(range(len(structure))) + output_parser.structure = structure.copy() + try: + output_parser.collect( + directory=working_directory, sorted_indices=sorted_indices + ) + except VaspCollectError: + raise + # Try getting high precision positions from CONTCAR + try: + output_parser.structure = get_final_structure_from_file( + working_directory=working_directory, + filename="CONTCAR", + structure=structure, + sorted_indices=sorted_indices, + ) + except (IOError, ValueError, FileNotFoundError): + pass + + # Bader analysis + if os.path.isfile(os.path.join(working_directory, "AECCAR0")) and os.path.isfile( + os.path.join(working_directory, "AECCAR2") + ): + bader = Bader(working_directory=working_directory, structure=structure) + try: + charges_orig, volumes_orig = bader.compute_bader_charges() + except ValueError: + warnings.warn("Invoking Bader charge analysis failed") + else: + charges, volumes = charges_orig.copy(), volumes_orig.copy() + charges[sorted_indices] = charges_orig + volumes[sorted_indices] = volumes_orig + if ( + "valence_charges" + in output_parser.generic_output.dft_log_dict.keys() + ): + 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, filename="CONTCAR", structure=None, sorted_indices=None): + """ + 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 = vasp_sorter(structure) + if structure is None: + try: + output_structure = read_atoms(filename=filename) + input_structure = output_structure.copy() + except (IndexError, ValueError, IOError): + raise IOError("Unable to read output structure") + else: + input_structure = structure.copy() + try: + output_structure = read_atoms( + filename=filename, + species_list=input_structure.get_parent_symbols(), + ) + input_structure.cell = output_structure.cell.copy() + input_structure.positions[sorted_indices] = ( + output_structure.positions + ) + except (IndexError, ValueError, IOError): + raise IOError("Unable to read output structure") + return input_structure From ee6a6debe184be082a4430f3623f88ce1dd1152e Mon Sep 17 00:00:00 2001 From: Jan Janssen Date: Mon, 10 Jun 2024 14:36:42 +0200 Subject: [PATCH 4/8] Vasp: Refactor bader analysis --- pyiron_atomistics/dft/bader.py | 31 ++++++++++++++++++++++++++----- pyiron_atomistics/vasp/base.py | 15 +++------------ 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/pyiron_atomistics/dft/bader.py b/pyiron_atomistics/dft/bader.py index c7260f8fd..6a1b17929 100644 --- a/pyiron_atomistics/dft/bader.py +++ b/pyiron_atomistics/dft/bader.py @@ -6,6 +6,9 @@ import os import subprocess +from pyiron_atomistics.vasp.volumetric_data import VaspVolumetricData + + __author__ = "Sudarsan Surendralal" __copyright__ = ( "Copyright 2021, Max-Planck-Institut für Eisenforschung GmbH - " @@ -26,22 +29,21 @@ class Bader: .. _Bader code: http://theory.cm.utexas.edu/henkelman/code/bader """ - def __init__(self, job): + def __init__(self, structure, working_directory): """ Initialize the Bader module Args: job (pyiron_atomistics.dft.job.generic.GenericDFTJob): A DFT job instance (finished/converged job) """ - self.job = job - self._working_directory = job.working_directory - self._structure = job.structure + self._working_directory = working_directory + self._structure = structure def _create_cube_files(self): """ Create CUBE format files of the total and valce charges to be used by the Bader program """ - cd_val, cd_total = self.job.get_valence_and_total_charge_density() + cd_val, cd_total = get_valence_and_total_charge_density(working_directory=self._working_directory) cd_val.write_cube_file( filename=os.path.join(self._working_directory, "valence_charge.CUBE") ) @@ -124,3 +126,22 @@ def parse_charge_vol_file(structure, filename="ACF.dat"): charges = np.genfromtxt(lines[2:], max_rows=len(structure))[:, 4] volumes = np.genfromtxt(lines[2:], max_rows=len(structure))[:, 6] return charges, volumes + + +def get_valence_and_total_charge_density(working_directory): + """ + Gives the valence and total charge densities + + Returns: + tuple: The required charge densities + """ + cd_core = VaspVolumetricData() + cd_total = VaspVolumetricData() + cd_val = VaspVolumetricData() + if os.path.isfile(working_directory + "/AECCAR0"): + cd_core.from_file(working_directory + "/AECCAR0") + cd_val.from_file(working_directory + "/AECCAR2") + cd_val.atoms = cd_val.atoms + cd_total.total_data = cd_core.total_data + cd_val.total_data + cd_total.atoms = cd_val.atoms + return cd_val, cd_total diff --git a/pyiron_atomistics/vasp/base.py b/pyiron_atomistics/vasp/base.py index cc42ac7ea..fb52020cf 100644 --- a/pyiron_atomistics/vasp/base.py +++ b/pyiron_atomistics/vasp/base.py @@ -40,7 +40,7 @@ electronic_structure_dict_to_hdf, ) from pyiron_atomistics.dft.waves.bandstructure import Bandstructure -from pyiron_atomistics.dft.bader import Bader +from pyiron_atomistics.dft.bader import Bader, get_valence_and_total_charge_density import warnings __author__ = "Sudarsan Surendralal, Felix Lochner" @@ -435,7 +435,7 @@ def collect_output_parser(self, cwd): if os.path.isfile(os.path.join(cwd, "AECCAR0")) and os.path.isfile( os.path.join(cwd, "AECCAR2") ): - bader = Bader(self) + bader = Bader(working_directory=self.working_directory, structure=self.structure) try: charges_orig, volumes_orig = bader.compute_bader_charges() except ValueError: @@ -1460,16 +1460,7 @@ def get_valence_and_total_charge_density(self): Returns: tuple: The required charge densities """ - cd_core = VaspVolumetricData() - cd_total = VaspVolumetricData() - cd_val = VaspVolumetricData() - if os.path.isfile(self.working_directory + "/AECCAR0"): - cd_core.from_file(self.working_directory + "/AECCAR0") - cd_val.from_file(self.working_directory + "/AECCAR2") - cd_val.atoms = cd_val.atoms - cd_total.total_data = cd_core.total_data + cd_val.total_data - cd_total.atoms = cd_val.atoms - return cd_val, cd_total + return get_valence_and_total_charge_density(working_directory=self.working_directory) def get_electrostatic_potential(self): """ From ccdc846751a084113054e8239f4c0e4a7630a556 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Mon, 10 Jun 2024 12:39:29 +0000 Subject: [PATCH 5/8] Format black --- pyiron_atomistics/dft/bader.py | 4 +++- pyiron_atomistics/vasp/output.py | 17 ++++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/pyiron_atomistics/dft/bader.py b/pyiron_atomistics/dft/bader.py index 6a1b17929..52508f61c 100644 --- a/pyiron_atomistics/dft/bader.py +++ b/pyiron_atomistics/dft/bader.py @@ -43,7 +43,9 @@ def _create_cube_files(self): """ Create CUBE format files of the total and valce charges to be used by the Bader program """ - cd_val, cd_total = get_valence_and_total_charge_density(working_directory=self._working_directory) + cd_val, cd_total = get_valence_and_total_charge_density( + working_directory=self._working_directory + ) cd_val.write_cube_file( filename=os.path.join(self._working_directory, "valence_charge.CUBE") ) diff --git a/pyiron_atomistics/vasp/output.py b/pyiron_atomistics/vasp/output.py index 5094f39b2..25736ecae 100644 --- a/pyiron_atomistics/vasp/output.py +++ b/pyiron_atomistics/vasp/output.py @@ -647,10 +647,7 @@ def parse_vasp_output(working_directory, structure=None, sorted_indices=None): charges, volumes = charges_orig.copy(), volumes_orig.copy() charges[sorted_indices] = charges_orig volumes[sorted_indices] = volumes_orig - if ( - "valence_charges" - in output_parser.generic_output.dft_log_dict.keys() - ): + if "valence_charges" in output_parser.generic_output.dft_log_dict.keys(): valence_charges = output_parser.generic_output.dft_log_dict[ "valence_charges" ] @@ -658,13 +655,13 @@ def parse_vasp_output(working_directory, structure=None, sorted_indices=None): output_parser.generic_output.dft_log_dict["bader_charges"] = ( valence_charges - charges ) - output_parser.generic_output.dft_log_dict["bader_volumes"] = ( - volumes - ) + output_parser.generic_output.dft_log_dict["bader_volumes"] = volumes return output_parser.to_dict() -def get_final_structure_from_file(working_directory, filename="CONTCAR", structure=None, sorted_indices=None): +def get_final_structure_from_file( + working_directory, filename="CONTCAR", structure=None, sorted_indices=None +): """ Get the final structure of the simulation usually from the CONTCAR file @@ -691,9 +688,7 @@ def get_final_structure_from_file(working_directory, filename="CONTCAR", structu species_list=input_structure.get_parent_symbols(), ) input_structure.cell = output_structure.cell.copy() - input_structure.positions[sorted_indices] = ( - output_structure.positions - ) + input_structure.positions[sorted_indices] = output_structure.positions except (IndexError, ValueError, IOError): raise IOError("Unable to read output structure") return input_structure From 86d1e7fe347a04f308e7ffe060dbf8e1b805c802 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Mon, 10 Jun 2024 12:39:50 +0000 Subject: [PATCH 6/8] Format black --- pyiron_atomistics/dft/bader.py | 4 +++- pyiron_atomistics/vasp/base.py | 8 ++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pyiron_atomistics/dft/bader.py b/pyiron_atomistics/dft/bader.py index 6a1b17929..52508f61c 100644 --- a/pyiron_atomistics/dft/bader.py +++ b/pyiron_atomistics/dft/bader.py @@ -43,7 +43,9 @@ def _create_cube_files(self): """ Create CUBE format files of the total and valce charges to be used by the Bader program """ - cd_val, cd_total = get_valence_and_total_charge_density(working_directory=self._working_directory) + cd_val, cd_total = get_valence_and_total_charge_density( + working_directory=self._working_directory + ) cd_val.write_cube_file( filename=os.path.join(self._working_directory, "valence_charge.CUBE") ) diff --git a/pyiron_atomistics/vasp/base.py b/pyiron_atomistics/vasp/base.py index fb52020cf..3a1c346f3 100644 --- a/pyiron_atomistics/vasp/base.py +++ b/pyiron_atomistics/vasp/base.py @@ -435,7 +435,9 @@ def collect_output_parser(self, cwd): if os.path.isfile(os.path.join(cwd, "AECCAR0")) and os.path.isfile( os.path.join(cwd, "AECCAR2") ): - bader = Bader(working_directory=self.working_directory, structure=self.structure) + bader = Bader( + working_directory=self.working_directory, structure=self.structure + ) try: charges_orig, volumes_orig = bader.compute_bader_charges() except ValueError: @@ -1460,7 +1462,9 @@ def get_valence_and_total_charge_density(self): Returns: tuple: The required charge densities """ - return get_valence_and_total_charge_density(working_directory=self.working_directory) + return get_valence_and_total_charge_density( + working_directory=self.working_directory + ) def get_electrostatic_potential(self): """ From 4882d8cb63e79fd13baa800c73095d4dfce45364 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jan=C3=9Fen?= Date: Tue, 11 Jun 2024 22:54:38 +0200 Subject: [PATCH 7/8] Add DocStrings --- pyiron_atomistics/vasp/base.py | 13 +++++++++++-- pyiron_atomistics/vasp/output.py | 12 ++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/pyiron_atomistics/vasp/base.py b/pyiron_atomistics/vasp/base.py index d989e13da..13e416a0b 100644 --- a/pyiron_atomistics/vasp/base.py +++ b/pyiron_atomistics/vasp/base.py @@ -392,7 +392,13 @@ def write_input(self): modified_elements=modified_elements, ) - def _store_output(self, output_dict): + def _store_output(self, output_dict: dict): + """ + Internal helper function to store the hierarchical output dictionary in the HDF5 file of the pyiron job object + + Args: + output_dict (dict): hierarchical output dictionary + """ output_dict_to_hdf( data_dict=output_dict, hdf=self._hdf5, @@ -404,7 +410,10 @@ def _store_output(self, output_dict): # define routines that collect all output files def collect_output(self): """ - Collects the outputs and stores them to the hdf file + The collect_output() method parses the output in the working_directory and stores it in the HDF5 file. It is + divided into two functions, the pyiron_atomistics.vasp.output.parse_vasp_output() function, which parses the + working directory and returns a dictionary with the output and the job._store_output() function which stores + the output dictionary in the HDF5 file. """ self._store_output( output_dict=parse_vasp_output( diff --git a/pyiron_atomistics/vasp/output.py b/pyiron_atomistics/vasp/output.py index 25736ecae..e577b4c52 100644 --- a/pyiron_atomistics/vasp/output.py +++ b/pyiron_atomistics/vasp/output.py @@ -600,9 +600,17 @@ def output_dict_to_hdf(data_dict, hdf, group_name="output"): dict_group_to_hdf(data_dict=data_dict, hdf=hdf5_output, group="outcar") -def parse_vasp_output(working_directory, structure=None, sorted_indices=None): +def parse_vasp_output(working_directory: str, structure: Atoms = None, sorted_indices: list = None) -> dict: """ - Collects the outputs and stores them to the hdf file + 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() if structure is None or len(structure) == 0: From 66ee8b4c4d75906fbcc3c6c8cb7650e2d96ee7f4 Mon Sep 17 00:00:00 2001 From: pyiron-runner Date: Wed, 12 Jun 2024 04:30:45 +0000 Subject: [PATCH 8/8] Format black --- pyiron_atomistics/vasp/output.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyiron_atomistics/vasp/output.py b/pyiron_atomistics/vasp/output.py index e577b4c52..2ade8bfeb 100644 --- a/pyiron_atomistics/vasp/output.py +++ b/pyiron_atomistics/vasp/output.py @@ -600,7 +600,9 @@ def output_dict_to_hdf(data_dict, hdf, group_name="output"): dict_group_to_hdf(data_dict=data_dict, hdf=hdf5_output, group="outcar") -def parse_vasp_output(working_directory: str, structure: Atoms = None, sorted_indices: list = None) -> dict: +def parse_vasp_output( + working_directory: str, structure: Atoms = None, sorted_indices: list = None +) -> dict: """ Parse the VASP output in the working_directory and return it as hierachical dictionary.