From b3c807da9f807ee9077608199ac44de61f974a30 Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 16:52:54 -0700 Subject: [PATCH 1/8] removing __getattr__ and writing allocating everything in the __postinit__ --- src/pymatgen/io/jdftx/outputs.py | 39 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/pymatgen/io/jdftx/outputs.py b/src/pymatgen/io/jdftx/outputs.py index b02bf331672..8983822bd92 100644 --- a/src/pymatgen/io/jdftx/outputs.py +++ b/src/pymatgen/io/jdftx/outputs.py @@ -202,6 +202,11 @@ def from_file(cls, file_path: str | Path, is_bgw: bool = False, none_slice_on_er ] return cls(slices=slices) + def __post_init__(self): + if len(self.slices): + self.electronic_output = self.slices[-1].electronic_output + self.t_s = self.slices[-1].t_s + def to_dict(self) -> dict: """ Convert the JDFTXOutfile object to a dictionary. @@ -1319,29 +1324,29 @@ def __len__(self) -> int: """ return len(self.slices) - def __getattr__(self, name: str) -> Any: - """Return attribute. + # def __getattr__(self, name: str) -> Any: + # """Return attribute. - Args: - name (str): The name of the attribute. + # Args: + # name (str): The name of the attribute. - Returns: - Any: The value of the attribute. + # Returns: + # Any: The value of the attribute. - Raises: - AttributeError: If the attribute is not found. - """ - if name in self.__dict__: - return self.__dict__[name] + # Raises: + # AttributeError: If the attribute is not found. + # """ + # if name in self.__dict__: + # return self.__dict__[name] - for cls in inspect.getmro(self.__class__): - if name in cls.__dict__ and isinstance(cls.__dict__[name], property): - return cls.__dict__[name].__get__(self) + # for cls in inspect.getmro(self.__class__): + # if name in cls.__dict__ and isinstance(cls.__dict__[name], property): + # return cls.__dict__[name].__get__(self) - if hasattr(self.slices[-1], name): - return getattr(self.slices[-1], name) + # if hasattr(self.slices[-1], name): + # return getattr(self.slices[-1], name) - raise AttributeError(f"{self.__class__.__name__} not found: {name}") + # raise AttributeError(f"{self.__class__.__name__} not found: {name}") def __str__(self) -> str: """Return string representation of JDFTXOutfile object. From 79fe430ea0797da84ae2db60658fd2fd64ae5cc1 Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 19:30:09 -0700 Subject: [PATCH 2/8] replaced all properties within JDFTXOutfile and JDFTXOutfileSlice as initialized attributes and fixed hidden errors revealed by doing so --- src/pymatgen/io/jdftx/jdftxoutfileslice.py | 479 +++---- src/pymatgen/io/jdftx/outputs.py | 1303 +++----------------- tests/io/jdftx/conftest.py | 11 +- tests/io/jdftx/test_jdftxoutfileslice.py | 19 +- 4 files changed, 345 insertions(+), 1467 deletions(-) diff --git a/src/pymatgen/io/jdftx/jdftxoutfileslice.py b/src/pymatgen/io/jdftx/jdftxoutfileslice.py index 5a466c83ef6..cafbe04dd0b 100644 --- a/src/pymatgen/io/jdftx/jdftxoutfileslice.py +++ b/src/pymatgen/io/jdftx/jdftxoutfileslice.py @@ -134,7 +134,7 @@ class JDFTXOutfileSlice: Properties: t_s (float | None): The total time in seconds for the calculation. - is_converged (bool | None): True if calculation converged. + converged (bool | None): True if calculation converged. trajectory (Trajectory): pymatgen Trajectory object containing intermediate Structure's of outfile slice calculation. electronic_output (dict): Dictionary with all relevant electronic information dumped from an eigstats log. @@ -254,261 +254,34 @@ class JDFTXOutfileSlice: has_parsable_pseudo: bool = False _total_electrons_backup: int | None = None + total_electrons: float | None = None _mu_backup: int | None = None - @property - def t_s(self) -> float | None: - """Return the total time in seconds for the calculation. - - Returns: - float: The total time in seconds for the calculation. - """ - t_s = None - if self.jstrucs: - t_s = self.jstrucs.t_s - return t_s - - @property - def is_converged(self) -> bool | None: - """Return True if calculation converged. - - Returns: - bool: True if the electronic and geometric optimization have converged (or only the former if a single-point - calculation). - """ - if self.jstrucs is None: - return None - converged = self.jstrucs.elec_converged - if self.geom_opt: - converged = converged and self.jstrucs.geom_converged - return converged - - @property - def trajectory(self) -> Trajectory: - """Return pymatgen trajectory object. - - Returns: - Trajectory: pymatgen Trajectory object containing intermediate Structure's of outfile slice calculation. - """ - constant_lattice = False - if self.jsettings_lattice is not None: - if "niterations" in self.jsettings_lattice.params: - constant_lattice = int(self.jsettings_lattice.params["niterations"]) == 0 - else: - raise ValueError("Unknown issue due to partial initialization of settings objects.") - return Trajectory.from_structures(structures=self.jstrucs, constant_lattice=constant_lattice) - - @property - def electronic_output(self) -> dict: - """Return a dictionary with all relevant electronic information. - - Returns: - dict: Dictionary with values corresponding to these keys in _electronic_output field. - """ - dct = {} - for field in self.__dataclass_fields__: - if field in self._electronic_output: - value = getattr(self, field) - dct[field] = value - return dct - - @property - def structure(self) -> Structure: - """Return calculation result as pymatgen Structure. - - Returns: - Structure: pymatgen Structure object. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs[-1] - raise AttributeError("Property structure inaccessible due to empty jstrucs class field") - - ########################################################################### - # Properties inherited directly from jstrucs - ########################################################################### - - @property - def eopt_type(self) -> str | None: - """ - Return eopt_type from most recent JOutStructure. - - Returns: - str | None: eopt_type from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.eopt_type - raise AttributeError("Property eopt_type inaccessible due to empty jstrucs class field") - - @property - def elecmindata(self) -> JElSteps: - """Return elecmindata from most recent JOutStructure. - - Returns: - JElSteps: elecmindata from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.elecmindata - raise AttributeError("Property elecmindata inaccessible due to empty jstrucs class field") - - @property - def stress(self) -> np.ndarray | None: - """Return stress from most recent JOutStructure. - - Returns: - np.ndarray | None: stress from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.stress - raise AttributeError("Property stress inaccessible due to empty jstrucs class field") - - @property - def strain(self) -> np.ndarray | None: - """Return strain from most recent JOutStructure. - - Returns: - np.ndarray | None: strain from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.strain - raise AttributeError("Property strain inaccessible due to empty jstrucs class field") - - @property - def nstep(self) -> int | None: - """Return (geometric) nstep from most recent JOutStructure. - - Returns: - int | None: (geometric) nstep from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.nstep - raise AttributeError("Property nstep inaccessible due to empty jstrucs class field") - - @property - def e(self) -> float | None: - """Return E from most recent JOutStructure. - - Returns: - float | None: E from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.e - raise AttributeError("Property e inaccessible due to empty jstrucs class field") - - @property - def grad_k(self) -> float | None: - """Return (geometric) grad_k from most recent JOutStructure. - - Returns: - float | None: (geometric) grad_k from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.grad_k - raise AttributeError("Property grad_k inaccessible due to empty jstrucs class field") - - @property - def alpha(self) -> float | None: - """Return (geometric) alpha from most recent JOutStructure. - - Returns: - float | None: (geometric) alpha from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.alpha - raise AttributeError("Property alpha inaccessible due to empty jstrucs class field") - - @property - def linmin(self) -> float | None: - """Return (geometric) linmin from most recent JOutStructure. - - Returns: - float | None: (geometric) linmin from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.linmin - raise AttributeError("Property linmin inaccessible due to empty jstrucs class field") - - @property - def nelectrons(self) -> float | None: - """Return nelectrons from most recent JOutStructure. - - Returns: - float | None: nelectrons from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.nelectrons - raise AttributeError("Property nelectrons inaccessible due to empty jstrucs class field") - - @property - def abs_magneticmoment(self) -> float | None: - """Return abs_magneticmoment from most recent JOutStructure. - - Returns: - float | None: abs_magneticmoment from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.abs_magneticmoment - raise AttributeError("Property abs_magneticmoment inaccessible due to empty jstrucs class field") - - @property - def tot_magneticmoment(self) -> float | None: - """Return tot_magneticmoment from most recent JOutStructure. - - Returns: - float | None: tot_magneticmoment from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.tot_magneticmoment - raise AttributeError("Property tot_magneticmoment inaccessible due to empty jstrucs class field") - - @property - def mu(self) -> float | None: - """Return mu from most recent JOutStructure. (Equivalent to efermi) - - Returns: - float | None: mu from most recent JOutStructure. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ + t_s: float | None = None + converged: bool | None = None + structure: Structure | None = None + trajectory: Trajectory | None = None + electronic_output: dict | None = None + eopt_type: str | None = None + elecmindata: JElSteps | None = None + stress: np.ndarray | None = None + strain: np.ndarray | None = None + nstep: int | None = None + e: float | None = None + grad_k: float | None = None + alpha: float | None = None + linmin: float | None = None + abs_magneticmoment: float | None = None + tot_magneticmoment: float | None = None + mu: float | None = None + elec_nstep: int | None = None + elec_e: float | None = None + elec_grad_k: float | None = None + elec_alpha: float | None = None + elec_linmin: float | None = None + + def _get_mu(self) -> None | float: + """Sets mu from most recent JOutStructure. (Equivalent to efermi)""" _mu = None if self.jstrucs is not None: _mu = self.jstrucs.mu @@ -516,81 +289,6 @@ def mu(self) -> float | None: _mu = self._mu_backup return _mu - ########################################################################### - # Electronic properties inherited from most recent JElSteps with symbol - # disambiguation. - ########################################################################### - - @property - def elec_nstep(self) -> int | None: - """Return the most recent electronic iteration. - - Returns: - int: The most recent electronic iteration. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.elec_nstep - raise AttributeError("Property elec_nstep inaccessible due to empty jstrucs class field") - - @property - def elec_e(self) -> float | None: - """Return the most recent electronic energy. - - Returns: - float: The most recent electronic energy. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.elec_e - raise AttributeError("Property elec_e inaccessible due to empty jstrucs class field") - - @property - def elec_grad_k(self) -> float | None: - """Return the most recent electronic grad_k. - - Returns: - float: The most recent electronic grad_k. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.elec_grad_k - raise AttributeError("Property elec_grad_k inaccessible due to empty jstrucs class field") - - @property - def elec_alpha(self) -> float | None: - """Return the most recent electronic alpha. - - Returns: - float: The most recent electronic alpha. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.elec_alpha - raise AttributeError("Property elec_alpha inaccessible due to empty jstrucs class field") - - @property - def elec_linmin(self) -> float | None: - """Return the most recent electronic linmin. - - Returns: - float: The most recent electronic linmin. - - Raises: - AttributeError: If the jstrucs class field is empty. - """ - if self.jstrucs is not None: - return self.jstrucs.elec_linmin - raise AttributeError("Property elec_linmin inaccessible due to empty jstrucs class field") - ########################################################################### # Creation methods ########################################################################### @@ -652,6 +350,7 @@ def _from_out_slice_init_all(self, text: list[str]) -> None: self._set_fluid(text) self._set_nbands(text) self._set_atom_vars(text) + self._set_total_electrons() self._set_pseudo_vars(text) self._set_lattice_vars(text) self.has_solvation = self._check_solvation() @@ -660,6 +359,91 @@ def _from_out_slice_init_all(self, text: list[str]) -> None: self.is_gc = key_exists("target-mu", text) self._set_ecomponents(text) + # Previously were properties, but are now set as attributes + self._from_out_slice_init_all_post_init() + + def _set_t_s(self) -> None: + """Return the total time in seconds for the calculation. + + Returns: + float: The total time in seconds for the calculation. + """ + _t_s = None + if self.jstrucs: + _t_s = self.jstrucs.t_s + self.t_s = _t_s + + def _set_converged(self) -> None: + """Return True if calculation converged. + + Returns: + bool: True if the electronic and geometric optimization have converged (or only the former if a single-point + calculation). + """ + if self.jstrucs is None: + return + converged = self.jstrucs.elec_converged + if self.geom_opt: + converged = converged and self.jstrucs.geom_converged + self.converged = converged + + def _set_trajectory(self) -> Trajectory: + """Return pymatgen trajectory object. + + Returns: + Trajectory: pymatgen Trajectory object containing intermediate Structure's of outfile slice calculation. + """ + constant_lattice = False + if self.jsettings_lattice is not None: + if "niterations" in self.jsettings_lattice.params: + constant_lattice = int(self.jsettings_lattice.params["niterations"]) == 0 + else: + raise ValueError("Unknown issue due to partial initialization of settings objects.") + self.trajectory = Trajectory.from_structures(structures=self.jstrucs, constant_lattice=constant_lattice) + + def _set_electronic_output(self) -> None: + """Return a dictionary with all relevant electronic information. + + Returns: + dict: Dictionary with values corresponding to these keys in _electronic_output field. + """ + dct = {} + for field in self._electronic_output: + if field in self.__dataclass_fields__: + value = getattr(self, field) + dct[field] = value + self.electronic_output = dct + + def _from_out_slice_init_all_post_init(self) -> None: + """Post init for running at end of "_from_out_slice_init_all" method. + + Sets class variables previously defined as properties. + """ + self._set_t_s() + self._set_converged() + self._set_electronic_output() + # + # if self.jstrucs is not None: + # self._set_trajectory() + # self.structure = self.jstrucs[-1] + # self.eopt_type = self.jstrucs.eopt_type + # self.elecmindata = self.jstrucs.elecmindata + # self.stress = self.jstrucs.stress + # self.strain = self.jstrucs.strain + # self.nstep = self.jstrucs.nstep + # self.e = self.jstrucs.e + # self.grad_k = self.jstrucs.grad_k + # self.alpha = self.jstrucs.alpha + # self.linmin = self.jstrucs.linmin + # self.abs_magneticmoment = self.jstrucs.abs_magneticmoment + # self.tot_magneticmoment = self.jstrucs.tot_magneticmoment + # self.mu = self._get_mu() + # self.elec_nstep = self.jstrucs.elec_nstep + # self.elec_e = self.jstrucs.elec_e + # self.elec_grad_k = self.jstrucs.elec_grad_k + # self.elec_alpha = self.jstrucs.elec_alpha + # self.elec_linmin = self.jstrucs.elec_linmin + def _get_xc_func(self, text: list[str]) -> str | None: """Get the exchange-correlation functional used in the calculation. @@ -882,7 +666,9 @@ def _set_eigvars(self, text: list[str]) -> None: self.lumo = eigstats["lumo"] self.emax = eigstats["emax"] self.egap = eigstats["egap"] - if (not self.has_eigstats) and (self.mu is not None): + if self.efermi is None: + if self.mu is None: + self.mu = self._get_mu() self.efermi = self.mu def _get_pp_type(self, text: list[str]) -> str | None: @@ -1069,7 +855,7 @@ def _set_geomopt_vars(self, text: list[str]) -> None: def _set_jstrucs(self, text: list[str]) -> None: """Set the jstrucs class variable. - Set the JStructures object to jstrucs from the out file text. + Set the JStructures object to jstrucs from the out file text and all class attributes initialized from jstrucs. Args: text (list[str]): Output of read_file for out file. @@ -1077,6 +863,26 @@ def _set_jstrucs(self, text: list[str]) -> None: self.jstrucs = JOutStructures._from_out_slice(text, opt_type=self.geom_opt_type) if self.etype is None: self.etype = self.jstrucs[-1].etype + if self.jstrucs is not None: + self._set_trajectory() + self.structure = self.jstrucs[-1] + self.eopt_type = self.jstrucs.eopt_type + self.elecmindata = self.jstrucs.elecmindata + self.stress = self.jstrucs.stress + self.strain = self.jstrucs.strain + self.nstep = self.jstrucs.nstep + self.e = self.jstrucs.e + self.grad_k = self.jstrucs.grad_k + self.alpha = self.jstrucs.alpha + self.linmin = self.jstrucs.linmin + self.abs_magneticmoment = self.jstrucs.abs_magneticmoment + self.tot_magneticmoment = self.jstrucs.tot_magneticmoment + self.mu = self._get_mu() + self.elec_nstep = self.jstrucs.elec_nstep + self.elec_e = self.jstrucs.elec_e + self.elec_grad_k = self.jstrucs.elec_grad_k + self.elec_alpha = self.jstrucs.elec_alpha + self.elec_linmin = self.jstrucs.elec_linmin def _set_backup_vars(self, text: list[str]) -> None: """Set backups for important variables. @@ -1174,13 +980,8 @@ def _set_fluid(self, text: list[str]) -> None: line = find_first_range_key("fluid ", text) self.fluid = text[line[0]].split()[1] - @property - def total_electrons(self) -> float | None: - """Return total_electrons from most recent JOutStructure. - - Returns: - float | None: Total electrons from most recent JOutStructure. - """ + def _set_total_electrons(self) -> None: + """Sets total_electrons from most recent JOutStructure.""" tot_elec = None if self.jstrucs is not None: _tot_elec = self.jstrucs.nelectrons @@ -1188,7 +989,7 @@ def total_electrons(self) -> float | None: tot_elec = _tot_elec if (tot_elec is None) and (self._total_electrons_backup is not None): tot_elec = self._total_electrons_backup - return tot_elec + self.total_electrons = tot_elec def _set_nbands(self, text: list[str]) -> None: """Set the Nbands class variable. diff --git a/src/pymatgen/io/jdftx/outputs.py b/src/pymatgen/io/jdftx/outputs.py index 8983822bd92..e2d54c9af88 100644 --- a/src/pymatgen/io/jdftx/outputs.py +++ b/src/pymatgen/io/jdftx/outputs.py @@ -15,6 +15,7 @@ class is written. from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any +from pymatgen.core.trajectory import Trajectory from pymatgen.io.jdftx._output_utils import read_outfile_slices from pymatgen.io.jdftx.jdftxoutfileslice import JDFTXOutfileSlice @@ -24,7 +25,6 @@ class is written. import numpy as np from pymatgen.core.structure import Structure - from pymatgen.core.trajectory import Trajectory from pymatgen.io.jdftx.jelstep import JElSteps from pymatgen.io.jdftx.jminsettings import ( JMinSettingsElectronic, @@ -53,8 +53,6 @@ class JDFTXOutfile: call of the JDFTx executable. Subsequent JDFTx calls within the same directory and prefix will append outputs to the same out file. More than one slice may correspond to restarted calculations, geom + single point calculations, or optimizations done with 3rd-party wrappers like ASE. - - Properties: prefix (str): The prefix of the most recent JDFTx call. jstrucs (JOutStructures): The JOutStructures object from the most recent JDFTx call. This object contains a series of JOutStructure objects in its 'slices' attribute, each corresponding to a single structure @@ -89,6 +87,7 @@ class JDFTXOutfile: geom_opt_type (str): The type of geometry optimization performed in the most recent JDFTx call. Options are 'lattice' or 'ionic' if geom_opt, else "single point". ('lattice' optimizations perform ionic optimizations as well unless ion positions are given in direct coordinates). + ecomponents (dict): The components of the total energy in eV of the most recent JDFTx call. efermi (float): The Fermi energy in eV of the most recent JDFTx call. Equivalent to "mu". egap (float): The band gap in eV of the most recent JDFTx call. (Only available if eigstats was dumped). emin (float): The minimum energy in eV (smallest Kohn-Sham eigenvalue) of the most recent JDFTx call. (Only @@ -104,7 +103,7 @@ class JDFTXOutfile: is_metal (bool): True if fillings of homo and lumo band-states are off-set by 1 and 0 by at least an arbitrary tolerance of 0.01 (ie 1 - 0.015 and 0.012 for homo/lumo fillings would be metallic, while 1-0.001 and 0 would not be). (Only available if eigstats was dumped). - is_converged (bool): True if most recent SCF cycle converged (and geom forces converged is calc is geom_opt) + converged (bool): True if most recent SCF cycle converged (and geom forces converged is calc is geom_opt) etype (str): String representation of total energy-type of system. Commonly "G" (grand-canonical potential) for GC calculations, and "F" for canonical (fixed electron count) calculations. broadening_type (str): Type of broadening for electronic filling about Fermi-level requested. Either "Fermi", @@ -179,6 +178,80 @@ class JDFTXOutfile: """ slices: list[JDFTXOutfileSlice] = field(default_factory=list) + prefix: str = field(init=False) + jstrucs: JOutStructures = field(init=False) + jsettings_fluid: JMinSettingsFluid = field(init=False) + jsettings_electronic: JMinSettingsElectronic = field(init=False) + jsettings_lattice: JMinSettingsLattice = field(init=False) + jsettings_ionic: JMinSettingsIonic = field(init=False) + xc_func: str = field(init=False) + lattice_initial: np.ndarray = field(init=False) + lattice_final: np.ndarray = field(init=False) + lattice: np.ndarray = field(init=False) + a: float = field(init=False) + b: float = field(init=False) + c: float = field(init=False) + fftgrid: list[int] = field(init=False) + geom_opt: bool = field(init=False) + geom_opt_type: str = field(init=False) + efermi: float = field(init=False) + egap: float = field(init=False) + emin: float = field(init=False) + emax: float = field(init=False) + homo: float = field(init=False) + lumo: float = field(init=False) + homo_filling: float = field(init=False) + lumo_filling: float = field(init=False) + is_metal: bool = field(init=False) + converged: bool = field(init=False) + etype: str = field(init=False) + broadening_type: str = field(init=False) + broadening: float = field(init=False) + kgrid: list[int] = field(init=False) + truncation_type: str = field(init=False) + truncation_radius: float = field(init=False) + pwcut: float = field(init=False) + rhocut: float = field(init=False) + pp_type: str = field(init=False) + total_electrons: float = field(init=False) + semicore_electrons: int = field(init=False) + valence_electrons: float = field(init=False) + total_electrons_uncharged: int = field(init=False) + semicore_electrons_uncharged: int = field(init=False) + valence_electrons_uncharged: int = field(init=False) + nbands: int = field(init=False) + atom_elements: list[str] = field(init=False) + atom_elements_int: list[int] = field(init=False) + atom_types: list[str] = field(init=False) + spintype: str = field(init=False) + nspin: int = field(init=False) + nat: int = field(init=False) + atom_coords_initial: list[list[float]] = field(init=False) + atom_coords_final: list[list[float]] = field(init=False) + atom_coords: list[list[float]] = field(init=False) + structure: Structure = field(init=False) + trajectory: Trajectory = field(init=False) + has_solvation: bool = field(init=False) + fluid: str = field(init=False) + is_gc: bool = field(init=False) + eopt_type: str = field(init=False) + elecmindata: JElSteps = field(init=False) + stress: np.ndarray = field(init=False) + strain: np.ndarray = field(init=False) + nstep: int = field(init=False) + e: float = field(init=False) + grad_k: float = field(init=False) + alpha: float = field(init=False) + linmin: float = field(init=False) + abs_magneticmoment: float = field(init=False) + tot_magneticmoment: float = field(init=False) + mu: float = field(init=False) + elec_nstep: int = field(init=False) + elec_e: float = field(init=False) + elec_grad_k: float = field(init=False) + elec_alpha: float = field(init=False) + elec_linmin: float = field(init=False) + electronic_output: float = field(init=False) @classmethod def from_file(cls, file_path: str | Path, is_bgw: bool = False, none_slice_on_error: bool = False) -> JDFTXOutfile: @@ -204,8 +277,97 @@ def from_file(cls, file_path: str | Path, is_bgw: bool = False, none_slice_on_er def __post_init__(self): if len(self.slices): + self.prefix = self.slices[-1].prefix + self.jstrucs = self.slices[-1].jstrucs + self.jsettings_fluid = self.slices[-1].jsettings_fluid + self.jsettings_electronic = self.slices[-1].jsettings_electronic + self.jsettings_lattice = self.slices[-1].jsettings_lattice + self.jsettings_ionic = self.slices[-1].jsettings_ionic + self.xc_func = self.slices[-1].xc_func + self.lattice_initial = self.slices[-1].lattice_initial + self.lattice_final = self.slices[-1].lattice_final + self.lattice = self.slices[-1].lattice + self.a = self.slices[-1].a + self.b = self.slices[-1].b + self.c = self.slices[-1].c + self.fftgrid = self.slices[-1].fftgrid + self.geom_opt = self.slices[-1].geom_opt + self.geom_opt_type = self.slices[-1].geom_opt_type + self.efermi = self.slices[-1].efermi + self.egap = self.slices[-1].egap + self.emin = self.slices[-1].emin + self.emax = self.slices[-1].emax + self.homo = self.slices[-1].homo + self.lumo = self.slices[-1].lumo + self.homo_filling = self.slices[-1].homo_filling + self.lumo_filling = self.slices[-1].lumo_filling + self.is_metal = self.slices[-1].is_metal + self.converged = self.slices[-1].converged + self.etype = self.slices[-1].etype + self.broadening_type = self.slices[-1].broadening_type + self.broadening = self.slices[-1].broadening + self.kgrid = self.slices[-1].kgrid + self.truncation_type = self.slices[-1].truncation_type + self.truncation_radius = self.slices[-1].truncation_radius + self.pwcut = self.slices[-1].pwcut + self.rhocut = self.slices[-1].rhocut + self.pp_type = self.slices[-1].pp_type + self.total_electrons = self.slices[-1].total_electrons + self.semicore_electrons = self.slices[-1].semicore_electrons + self.valence_electrons = self.slices[-1].valence_electrons + self.total_electrons_uncharged = self.slices[-1].total_electrons_uncharged + self.semicore_electrons_uncharged = self.slices[-1].semicore_electrons_uncharged + self.valence_electrons_uncharged = self.slices[-1].valence_electrons_uncharged + self.nbands = self.slices[-1].nbands + self.atom_elements = self.slices[-1].atom_elements + self.atom_elements_int = self.slices[-1].atom_elements_int + self.atom_types = self.slices[-1].atom_types + self.spintype = self.slices[-1].spintype + self.nspin = self.slices[-1].nspin + self.nat = self.slices[-1].nat + self.atom_coords_initial = self.slices[-1].atom_coords_initial + self.atom_coords_final = self.slices[-1].atom_coords_final + self.atom_coords = self.slices[-1].atom_coords + self.structure = self.slices[-1].structure + self.trajectory = self._get_trajectory() + self.has_solvation = self.slices[-1].has_solvation + self.fluid = self.slices[-1].fluid + self.is_gc = self.slices[-1].is_gc + self.eopt_type = self.slices[-1].eopt_type + self.elecmindata = self.slices[-1].elecmindata + self.stress = self.slices[-1].stress + self.strain = self.slices[-1].strain + self.nstep = self.slices[-1].nstep + self.e = self.slices[-1].e + self.grad_k = self.slices[-1].grad_k + self.alpha = self.slices[-1].alpha + self.linmin = self.slices[-1].linmin + self.abs_magneticmoment = self.slices[-1].abs_magneticmoment + self.tot_magneticmoment = self.slices[-1].tot_magneticmoment + self.mu = self.slices[-1].mu + self.elec_nstep = self.slices[-1].elec_nstep + self.elec_e = self.slices[-1].elec_e + self.elec_grad_k = self.slices[-1].elec_grad_k + self.elec_alpha = self.slices[-1].elec_alpha + self.elec_linmin = self.slices[-1].elec_linmin self.electronic_output = self.slices[-1].electronic_output self.t_s = self.slices[-1].t_s + self.ecomponents = self.slices[-1].ecomponents + + def _get_trajectory(self) -> Trajectory: + """Set the trajectory attribute of the JDFTXOutfile object.""" + constant_lattice = True + structures = [] + for _i, slc in enumerate(self.slices): + structures += slc.jstrucs.slices + if constant_lattice and (slc.jsettings_lattice is not None): + if "niterations" in slc.jsettings_lattice.params: + if int(slc.jsettings_lattice.params["niterations"]) > 1: + constant_lattice = False + else: + constant_lattice = False + + return Trajectory.from_structures(structures=structures, constant_lattice=constant_lattice) def to_dict(self) -> dict: """ @@ -216,1138 +378,47 @@ def to_dict(self) -> dict: """ dct = {} for fld in self.__dataclass_fields__: - value = getattr(self, fld) - dct[fld] = value + if hasattr(self, fld): + value = getattr(self, fld) + dct[fld] = value for name, _obj in inspect.getmembers(type(self), lambda o: isinstance(o, property)): dct[name] = getattr(self, name) return dct ########################################################################### - # Properties inherited from most recent JDFTXOutfileSlice + # Magic methods ########################################################################### - @property - def prefix(self) -> str: - """ - The prefix of the most recent JDFTx call. - - Returns: - str: The prefix from the most recent JOutStructure. - """ - if len(self.slices): - return self.slices[-1].prefix - raise AttributeError("Property prefix inaccessible due to empty slices class field") - - @property - def jstrucs(self) -> JOutStructures: - """ - Return jstrucs from most recent JOutStructure. - - Returns: - JOutStructures: The JOutStructures object from the most recent JDFTx call. - """ - if len(self.slices): - return self.slices[-1].jstrucs - raise AttributeError("Property jstrucs inaccessible due to empty slices class field") - - @property - def jsettings_fluid( - self, - ) -> JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: - """ - Return jsettings_fluid from most recent JOutStructure. - - Returns: - JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: The JMinSettingsFluid - object from the most recent JDFTx call. - """ - if len(self.slices): - return self.slices[-1].jsettings_fluid - raise AttributeError("Property jsettings_fluid inaccessible due to empty slices class field") - - @property - def jsettings_electronic( - self, - ) -> JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: - """ - Return jsettings_electronic from most recent JOutStructure. - - Returns: - JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: The - JMinSettingsElectronic object from the most recent JDFTx call. - """ - if len(self.slices): - return self.slices[-1].jsettings_electronic - raise AttributeError("Property jsettings_electronic inaccessible due to empty slices class field") - - @property - def jsettings_lattice( - self, - ) -> JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: - """ - Return jsettings_lattice from most recent JOutStructure. - - Returns: - JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: The - JMinSettingsLattice object from the most recent JDFTx call. - """ - if len(self.slices): - return self.slices[-1].jsettings_lattice - raise AttributeError("Property jsettings_lattice inaccessible due to empty slices class field") - - @property - def jsettings_ionic( - self, - ) -> JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: - """ - Return jsettings_ionic from most recent JOutStructure. - - Returns: - JMinSettingsFluid | JMinSettingsElectronic | JMinSettingsLattice | JMinSettingsIonic: The JMinSettingsIonic - object from the most recent JDFTx call. - """ - if len(self.slices): - return self.slices[-1].jsettings_ionic - raise AttributeError("Property jsettings_ionic inaccessible due to empty slices class field") - - @property - def xc_func(self) -> str: - """ - Return xc_func from most recent JOutStructure. - - Returns: - str: The name of the exchange correlation functional used for the calculation. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].xc_func - raise AttributeError("Property xc_func inaccessible due to empty slices class field") - - @property - def lattice_initial(self) -> np.ndarray: - """ - Returns the initial lattice vectors from the most recent JOutStructure. - - Returns: - np.ndarray: The initial lattice vectors. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].lattice_initial - raise AttributeError("Property lattice_initial inaccessible due to empty slices class field") - - @property - def lattice_final(self) -> np.ndarray: - """ - Returns the final lattice vectors from the most recent JOutStructure. - - Returns: - np.ndarray: The final lattice vectors. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].lattice_final - raise AttributeError("Property lattice_final inaccessible due to empty slices class field") - - @property - def lattice(self) -> np.ndarray: - """ - Returns the lattice vectors from the most recent JOutStructure. - - Returns: - np.ndarray: The lattice vectors. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].lattice - raise AttributeError("Property lattice inaccessible due to empty slices class field") - - @property - def a(self) -> float: - """ - Returns the length of the first lattice vector from the most recent JOutStructure. - - Returns: - float: The length of the first lattice vector in Angstroms. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].a - raise AttributeError("Property a inaccessible due to empty slices class field") - - @property - def b(self) -> float: - """ - Returns the length of the second lattice vector from the most recent JOutStructure. - - Returns: - float: The length of the second lattice vector in Angstroms. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].b - raise AttributeError("Property b inaccessible due to empty slices class field") - - @property - def c(self) -> float: - """ - Returns the length of the third lattice vector from the most recent JOutStructure. - - Returns: - float: The length of the third lattice vector in Angstroms. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].c - raise AttributeError("Property c inaccessible due to empty slices class field") - - @property - def fftgrid(self) -> list[int]: - """ - Returns the FFT grid shape from the most recent JOutStructure. - - Returns: - list[int]: The shape of the electronic density array. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].fftgrid - raise AttributeError("Property fftgrid inaccessible due to empty slices class field") - - @property - def geom_opt(self) -> bool: - """ - Returns whether the most recent JOutStructure included a geometric optimization. - - Returns: - bool: True if the calculation included a geometric optimization, False otherwise. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].geom_opt - raise AttributeError("Property geom_opt inaccessible due to empty slices class field") - - @property - def geom_opt_type(self) -> str: - """ - Return geom_opt_type from most recent JOutStructure. - - Returns: - str: The type of geometric optimization performed (lattice, ionic, or single point). - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].geom_opt_type - raise AttributeError("Property geom_opt_type inaccessible due to empty slices class field") - - @property - def efermi(self) -> float | None: - """ - Return efermi from most recent JOutStructure. - - Returns: - float | None: The energy of the Fermi level in eV. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].efermi - raise AttributeError("Property efermi inaccessible due to empty slices class field") - - @property - def egap(self) -> float | None: - """ - Return egap from most recent JOutStructure. - - Returns: - float | None: The size of the band gap in eV. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].egap - raise AttributeError("Property egap inaccessible due to empty slices class field") - - @property - def emin(self) -> float | None: - """ - Return emin from most recent JOutStructure. - - Returns: - float | None: The lowest Kohn-Sham eigenvalue in eV. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].emin - raise AttributeError("Property emin inaccessible due to empty slices class field") - - @property - def emax(self) -> float | None: - """ - Return emax from most recent JOutStructure. - - Returns: - float | None: The highest Kohn-Sham eigenvalue in eV. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].emax - raise AttributeError("Property emax inaccessible due to empty slices class field") - - @property - def homo(self) -> float | None: - """ - Return homo from most recent JOutStructure. - - Returns: - float | None: The energy of last band-state before Fermi level (Highest Occupied Molecular Orbital). - None if eigstats are not dumped. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].homo - raise AttributeError("Property homo inaccessible due to empty slices class field") - - @property - def lumo(self) -> float | None: - """ - Return lumo from most recent JOutStructure. - - Returns: - float | None: The energy of first band-state after Fermi level (Lowest Unoccupied Molecular Orbital). - None if eigstats are not dumped. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].lumo - raise AttributeError("Property lumo inaccessible due to empty slices class field") - - @property - def homo_filling(self) -> float | None: - """ - Return homo_filling from most recent JOutStructure. - - Returns: - float | None: The filling at the "homo" energy level. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].homo_filling - raise AttributeError("Property homo_filling inaccessible due to empty slices class field") - - @property - def lumo_filling(self) -> float | None: - """ - Return lumo_filling from most recent JOutStructure. - - Returns: - float | None: The filling at the "lumo" energy level. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].lumo_filling - raise AttributeError("Property lumo_filling inaccessible due to empty slices class field") + def __getitem__(self, key: int | str) -> JDFTXOutfileSlice | Any: + """Return item. - @property - def is_metal(self) -> bool | None: - """ - Return is_metal from most recent JOutStructure. + Args: + key (int | str): The key of the item. Returns: - bool | None: True if fillings of homo and lumo band-states are off-set by 1 and 0 by at least an arbitrary - tolerance of 0.01. None if eigstats are not dumped. + JDFTXOutfileSlice | Any: The value of the item. Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].is_metal - raise AttributeError("Property is_metal inaccessible due to empty slices class field") - - @property - def is_converged(self) -> bool | None: - """Return True if calculation converged. - - Returns: - bool: True if the electronic and geometric optimization have converged (or only the former if a single-point - calculation). - """ - if len(self.slices): - return self.slices[-1].is_converged - raise AttributeError("Property is_converged inaccessible due to empty slices class field") - - @property - def etype(self) -> str | None: - """ - Return etype from most recent JOutStructure. - - Returns: - str | None: The string representation of the energy type by which the electronic ensemble was minimized - (G, grand-canonical potential for grand-canonical ensemble; F, Helmholtz, for canonical ensemble). - """ - if len(self.slices): - return self.slices[-1].etype - raise AttributeError("Property etype inaccessible due to empty slices class field") - - @property - def broadening_type(self) -> str: - """ - Return broadening_type from most recent JOutStructure. - - Returns: - str: The function used for smearing electronic filling about the Fermi level. - """ - if len(self.slices): - return self.slices[-1].broadening_type - raise AttributeError("Property broadening_type inaccessible due to empty slices class field") - - @property - def broadening(self) -> float: - """ - Return broadening from most recent JOutStructure. - - Returns: - float: The parameter controlling the magnitude of broadening of electronic filling about the Fermi level. - """ - if len(self.slices): - return self.slices[-1].broadening - raise AttributeError("Property broadening inaccessible due to empty slices class field") - - @property - def kgrid(self) -> list: - """ - Return kgrid from most recent JOutStructure. - - Returns: - list: The shape of the k-point mesh used to sample the Brillouin-zone of the unit cell (equivalent to kpoint - folding). - """ - if len(self.slices): - return self.slices[-1].kgrid - raise AttributeError("Property kgrid inaccessible due to empty slices class field") - - @property - def truncation_type(self) -> str: - """ - Return truncation_type from most recent JOutStructure. - - Returns: - str: The type of Coloumb truncation used to avoid interaction with neighboring periodic images. ("Periodic" - if no truncation) - """ - if len(self.slices): - return self.slices[-1].truncation_type - raise AttributeError("Property truncation_type inaccessible due to empty slices class field") - - @property - def truncation_radius(self) -> float | None: - """ - Return truncation_radius from most recent JOutStructure. - - Returns: - float | None: The radius of coloumb truncation boundary in Bohr (not None iff truncation_type is spherical). - """ - if len(self.slices): - return self.slices[-1].truncation_radius - raise AttributeError("Property truncation_radius inaccessible due to empty slices class field") - - @property - def pwcut(self) -> float: - """ - Return pwcut from most recent JOutStructure. - - Returns: - float: The energy cutoff for planewaves entering the basis set in Hartree. - """ - if len(self.slices): - return self.slices[-1].pwcut - raise AttributeError("Property pwcut inaccessible due to empty slices class field") - - @property - def rhocut(self) -> float: - """ - Return rhocut from most recent JOutStructure. - - Returns: - float: The energy cutoff for the resolution of the real-space grid in Hartree. + TypeError: If the key type is invalid. """ - if len(self.slices): - return self.slices[-1].rhocut - raise AttributeError("Property rhocut inaccessible due to empty slices class field") + val = None + if type(key) is int: + val = self.slices[key] + elif type(key) is str: + val = getattr(self, key) + else: + raise TypeError(f"Invalid key type: {type(key)}") + return val - @property - def pp_type(self) -> str | None: - """ - Return pp_type from most recent JOutStructure. + def __len__(self) -> int: + """Return length of JDFTXOutfile object. Returns: - str | None: The name of the pseudopotential library used for the calculation. Only "GBRV" and "SG15" are - supported by this output parser, otherwise pp_type is None. - """ - if len(self.slices): - return self.slices[-1].pp_type - raise AttributeError("Property pp_type inaccessible due to empty slices class field") - - @property - def total_electrons(self) -> float: - """ - Return total_electrons from most recent JOutStructure. - - Returns: - float: The total number of electrons. - """ - if len(self.slices): - return self.slices[-1].total_electrons - raise AttributeError("Property total_electrons inaccessible due to empty slices class field") - - @property - def semicore_electrons(self) -> int: - """ - Return semicore_electrons from most recent JOutStructure. - - Returns: - int: The number of semicore electrons discluded from pseudopotentials but not part of the atom's valence - shell. - """ - if len(self.slices): - return self.slices[-1].semicore_electrons - raise AttributeError("Property semicore_electrons inaccessible due to empty slices class field") - - @property - def valence_electrons(self) -> float: - """ - Return valence_electrons from most recent JOutStructure. - - Returns: - float: The number of valence electrons. - """ - if len(self.slices): - return self.slices[-1].valence_electrons - raise AttributeError("Property valence_electrons inaccessible due to empty slices class field") - - @property - def total_electrons_uncharged(self) -> int: - """ - Return total_electrons_uncharged from most recent JOutStructure. - - Returns: - int: The number of electrons required to reach a neutral cell charge. - """ - if len(self.slices): - return self.slices[-1].total_electrons_uncharged - raise AttributeError("Property total_electrons_uncharged inaccessible due to empty slices class field") - - @property - def semicore_electrons_uncharged(self) -> int: - """ - Return semicore_electrons_uncharged from most recent JOutStructure. - - Returns: - int: The number of semicore electrons uncharged. - """ - if len(self.slices): - return self.slices[-1].semicore_electrons_uncharged - raise AttributeError("Property semicore_electrons_uncharged inaccessible due to empty slices class field") - - @property - def valence_electrons_uncharged(self) -> int: - """ - Return valence_electrons_uncharged from most recent JOutStructure. - - Returns: - int: The number of valence electrons uncharged. - """ - if len(self.slices): - return self.slices[-1].valence_electrons_uncharged - raise AttributeError("Property valence_electrons_uncharged inaccessible due to empty slices class field") - - @property - def nbands(self) -> int: - """ - Returns the number of bands used in the calculation from the most recent JOutStructure. - - Returns: - int: The number of bands used in the calculation. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].nbands - raise AttributeError("Property nbands inaccessible due to empty slices class field") - - @property - def atom_elements(self) -> list[str]: - """ - Returns the list of each ion's element symbol in the most recent JDFTx call from the most recent JOutStructure. - - Returns: - list[str]: The list of each ion's element symbol in the most recent JDFTx call. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].atom_elements - raise AttributeError("Property atom_elements inaccessible due to empty slices class field") - - @property - def atom_elements_int(self) -> list[int]: - """ - Returns the list of ion's atomic numbers in the most recent JDFTx call from the most recent JOutStructure. - - Returns: - list[int]: The list of ion's atomic numbers in the most recent JDFTx call. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].atom_elements_int - raise AttributeError("Property atom_elements_int inaccessible due to empty slices class field") - - @property - def atom_types(self) -> list: - """ - Returns the non-repeating list of each ion's element symbol in the most recent JDFTx call from the most recent - JOutStructure. - - Returns: - list: The non-repeating list of each ion's element symbol in the most recent JDFTx call. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].atom_types - raise AttributeError("Property atom_types inaccessible due to empty slices class field") - - @property - def spintype(self) -> str: - """ - Returns the way spin was incorporated in the most recent JDFTx call from the most recent JOutStructure. - - Returns: - str: The way spin was incorporated in the most recent JDFTx call. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].spintype - raise AttributeError("Property spintype inaccessible due to empty slices class field") - - @property - def nspin(self) -> int: - """ - Returns the number of spins used in the calculation from the most recent JOutStructure. - - Returns: - int: The number of spins used in the calculation. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].nspin - raise AttributeError("Property nspin inaccessible due to empty slices class field") - - @property - def nat(self) -> int: - """ - Returns the number of atoms in the most recent JDFTx call from the most recent JOutStructure. - - Returns: - int: The number of atoms in the most recent JDFTx call. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].nat - raise AttributeError("Property nat inaccessible due to empty slices class field") - - @property - def atom_coords_initial(self) -> list[list[float]]: - """ - Returns the initial atomic coordinates from the most recent JOutStructure. - - Returns: - list[list[float]]: The initial atomic coordinates. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].atom_coords_initial - raise AttributeError("Property atom_coords_initial inaccessible due to empty slices class field") - - @property - def atom_coords_final(self) -> list[list[float]]: - """ - Returns the final atomic coordinates from the most recent JOutStructure. - - Returns: - list[list[float]]: The final atomic coordinates. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].atom_coords_final - raise AttributeError("Property atom_coords_final inaccessible due to empty slices class field") - - @property - def atom_coords(self) -> list[list[float]]: - """ - Returns the atomic coordinates from the most recent JOutStructure. - - Returns: - list[list[float]]: The atomic coordinates. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].atom_coords - raise AttributeError("Property atom_coords inaccessible due to empty slices class field") - - @property - def structure(self) -> Structure: - """ - Returns the structure from the most recent JOutStructure. - - Returns: - Structure: The structure from the most recent JOutStructure. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].structure - raise AttributeError("Property structure inaccessible due to empty slices class field") - - @property - def trajectory(self) -> Trajectory: - """ - Returns the trajectory from the most recent JOutStructure. - - Returns: - Trajectory: The trajectory from the most recent JOutStructure. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].trajectory - raise AttributeError("Property trajectory inaccessible due to empty slices class field") - - @property - def has_solvation(self) -> bool: - """ - Returns whether the most recent JDFTx call included a solvation calculation from the most recent JOutStructure. - - Returns: - bool: True if the most recent JDFTx call included a solvation calculation, False otherwise. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].has_solvation - raise AttributeError("Property has_solvation inaccessible due to empty slices class field") - - @property - def fluid(self) -> str: - """ - Returns the name of the implicit solvent used in the calculation from the most recent JOutStructure. - - Returns: - str: The name of the implicit solvent used in the calculation. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].fluid - raise AttributeError("Property fluid inaccessible due to empty slices class field") - - @property - def is_gc(self) -> bool: - """ - Returns whether the most recent slice is a grand canonical calculation from the most recent JOutStructure. - - Returns: - bool: True if the most recent slice is a grand canonical calculation, False otherwise. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].is_gc - raise AttributeError("Property is_gc inaccessible due to empty slices class field") - - ########################################################################### - # Properties inherited from most recent JDFTXOutfileSlice directly through - # the JDFTXOutfileSlice object's jstrucs class variable. - ########################################################################### - - @property - def eopt_type(self) -> str: - """ - Returns the eopt_type from the most recent JOutStructure. - - Returns: - str: The eopt_type from the most recent JOutStructure. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].eopt_type - raise AttributeError("Property eopt_type inaccessible due to empty jstrucs class field") - - @property - def elecmindata(self) -> JElSteps: - """ - Returns the elecmindata from the most recent JOutStructure. - - Returns: - JElSteps: The elecmindata from the most recent JOutStructure. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].elecmindata - raise AttributeError("Property elecmindata inaccessible due to empty jstrucs class field") - - @property - def stress(self) -> np.ndarray: - """ - Returns the stress tensor from the most recent JOutStructure. - - Returns: - np.ndarray: The stress tensor of the unit cell in units eV/A^3. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].stress - raise AttributeError("Property stress inaccessible due to empty jstrucs class field") - - @property - def strain(self) -> np.ndarray: - """ - Returns the strain tensor from the most recent JOutStructure. - - Returns: - np.ndarray: The unitless strain tensor. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].strain - raise AttributeError("Property strain inaccessible due to empty jstrucs class field") - - @property - def nstep(self) -> int: - """ - Returns the (geometric) step number from the most recent JOutStructure. - - Returns: - int: The (geometric) step number from the most recent JOutStructure. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].nstep - raise AttributeError("Property nstep inaccessible due to empty jstrucs class field") - - @property - def e(self) -> float: - """ - Returns the energy from the most recent JOutStructure. - - Returns: - float: The energy of the system's etype in eV. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].e - raise AttributeError("Property e inaccessible due to empty jstrucs class field") - - @property - def grad_k(self) -> float: - """ - Returns the (geometric) grad_k from the most recent JOutStructure. - - Returns: - float: The final norm of the preconditioned gradient for geometric optimization of the most recent JDFTx - call (evaluated as dot(g, Kg), where g is the gradient and Kg is the preconditioned gradient). - (written as "|grad|_K" in JDFTx output). - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].grad_k - raise AttributeError("Property grad_k inaccessible due to empty jstrucs class field") - - @property - def alpha(self) -> float: - """ - Returns the (geometric) alpha from the most recent JOutStructure. - - Returns: - float: The geometric step size along the line minimization. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].alpha - raise AttributeError("Property alpha inaccessible due to empty jstrucs class field") - - @property - def linmin(self) -> float: - """ - Returns the (geometric) linmin from the most recent JOutStructure. - - Returns: - float: The final normalized projection of the geometric step direction onto the gradient for the most recent - JDFTx call. - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].linmin - raise AttributeError("Property linmin inaccessible due to empty jstrucs class field") - - @property - def nelectrons(self) -> float: - """ - Returns the nelectrons from the most recent JOutStructure. - - Returns: - float: The number of electrons (equivalent to total_electrons). - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].nelectrons - raise AttributeError("Property nelectrons inaccessible due to empty jstrucs class field") - - @property - def abs_magneticmoment(self) -> float | None: - """ - Returns the abs_magneticmoment from the most recent JOutStructure. - - Returns: - float | None: The absolute magnetic moment of electronic density. (None if restricted spin) - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].abs_magneticmoment - raise AttributeError("Property abs_magneticmoment inaccessible due to empty jstrucs class field") - - @property - def tot_magneticmoment(self) -> float | None: - """ - Returns the tot_magneticmoment from the most recent JOutStructure. - - Returns: - float | None: The total magnetic moment of the electronic density. (None if restricted spin) - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].tot_magneticmoment - raise AttributeError("Property tot_magneticmoment inaccessible due to empty jstrucs class field") - - @property - def mu(self) -> float: - """ - Returns the mu from the most recent JOutStructure. - - Returns: - float: The mu from the most recent JOutStructure. (Equivalent to efermi) - - Raises: - AttributeError: If the slices class field is empty. - """ - if len(self.slices): - return self.slices[-1].mu - raise AttributeError("Property mu inaccessible due to empty jstrucs class field") - - ########################################################################### - # Electronic properties with symbol disambiguation inherited from most - # recent JDFTXOutfileSlice directly through the JDFTXOutfileSlice - # object's jstrucs class variable. - ########################################################################### - - @property - def elec_nstep(self) -> int: - """ - Return the most recent electronic step number. - - Returns: - int: The most recent electronic step number. - """ - if len(self.slices): - return self.slices[-1].elec_nstep - raise AttributeError("Property elec_inter inaccessible due to empty jstrucs class field") - - @property - def elec_e(self) -> float: - """ - Return the most recent electronic energy. - - Returns: - float: The most recent electronic energy. - """ - if len(self.slices): - return self.slices[-1].elec_e - raise AttributeError("Property elec_e inaccessible due to empty jstrucs class field") - - @property - def elec_grad_k(self) -> int: - """ - Return the most recent electronic grad_k. (Equivalent to grad_k but for electronic line minimization) - - Returns: - float: The most recent electronic grad_k. - """ - if len(self.slices): - return self.slices[-1].elec_grad_k - raise AttributeError("Property elec_grad_k inaccessible due to empty jstrucs class field") - - @property - def elec_alpha(self) -> float: - """ - Return the most recent electronic alpha. (Equivalent to alpha but for electronic line minimization) - - Returns: - float: The most recent electronic alpha. - """ - if len(self.slices): - return self.slices[-1].elec_alpha - raise AttributeError("Property elec_alpha inaccessible due to empty jstrucs class field") - - @property - def elec_linmin(self) -> float: - """ - Return the most recent electronic linmin. (Equivalent to linmin but for electronic line minimization) - - Returns: - float: The most recent electronic linmin. - """ - if len(self.slices): - return self.slices[-1].elec_linmin - raise AttributeError("Property elec_linmin inaccessible due to empty jstrucs class field") - - ########################################################################### - # Magic methods - ########################################################################### - - def __getitem__(self, key: int | str) -> JDFTXOutfileSlice | Any: - """Return item. - - Args: - key (int | str): The key of the item. - - Returns: - JDFTXOutfileSlice | Any: The value of the item. - - Raises: - TypeError: If the key type is invalid. - """ - val = None - if type(key) is int: - val = self.slices[key] - elif type(key) is str: - val = getattr(self, key) - else: - raise TypeError(f"Invalid key type: {type(key)}") - return val - - def __len__(self) -> int: - """Return length of JDFTXOutfile object. - - Returns: - int: The number of geometric optimization steps in the JDFTXOutfile object. + int: The number of geometric optimization steps in the JDFTXOutfile object. """ return len(self.slices) - # def __getattr__(self, name: str) -> Any: - # """Return attribute. - - # Args: - # name (str): The name of the attribute. - - # Returns: - # Any: The value of the attribute. - - # Raises: - # AttributeError: If the attribute is not found. - # """ - # if name in self.__dict__: - # return self.__dict__[name] - - # for cls in inspect.getmro(self.__class__): - # if name in cls.__dict__ and isinstance(cls.__dict__[name], property): - # return cls.__dict__[name].__get__(self) - - # if hasattr(self.slices[-1], name): - # return getattr(self.slices[-1], name) - - # raise AttributeError(f"{self.__class__.__name__} not found: {name}") - def __str__(self) -> str: """Return string representation of JDFTXOutfile object. diff --git a/tests/io/jdftx/conftest.py b/tests/io/jdftx/conftest.py index 317d006df69..20f7f82aa21 100644 --- a/tests/io/jdftx/conftest.py +++ b/tests/io/jdftx/conftest.py @@ -137,8 +137,9 @@ def jdftxoutfile_matches_known(joutfile: JDFTXOutfile, known: dict): assert joutfile.trajectory is not None assert joutfile.electronic_output is not None assert joutfile.structure is not None - joutfile[-1].jstrucs = None - assert joutfile.is_converged is None + # # Commenting out as we are no longer accessing "is_converged" as a property + # joutfile[-1].jstrucs = None + # assert joutfile.is_converged is None example_sp_outfile_path = ex_out_files_dir / Path("example_sp.out") @@ -179,7 +180,7 @@ def jdftxoutfile_matches_known(joutfile: JDFTXOutfile, known: dict): "nbands": 174, "nat": 16, "t_s": 165.87, - "opt_type": None, + "geom_opt_type": "single point", "prefix": "jdft", "etype": "F", "converged": True, @@ -223,7 +224,7 @@ def jdftxoutfile_matches_known(joutfile: JDFTXOutfile, known: dict): "nbands": 42, "nat": 8, "t_s": 314.16, - "opt_type": "LatticeMinimize", + "geom_opt_type": "lattice", "prefix": "$VAR", "etype": "F", "converged": True, @@ -268,7 +269,7 @@ def jdftxoutfile_matches_known(joutfile: JDFTXOutfile, known: dict): "nbands": 195, "nat": 41, "t_s": 2028.57, - "opt_type": "IonicMinimize", + "geom_opt_type": "ionic", "prefix": "$VAR", "etype": "G", "converged": True, diff --git a/tests/io/jdftx/test_jdftxoutfileslice.py b/tests/io/jdftx/test_jdftxoutfileslice.py index b25df6e472e..b12ad4e847f 100644 --- a/tests/io/jdftx/test_jdftxoutfileslice.py +++ b/tests/io/jdftx/test_jdftxoutfileslice.py @@ -26,7 +26,7 @@ def test_jdftxoutfileslice_stringify(): def test_jdftxoutfileslice_converge(): joutslice = JDFTXOutfileSlice._from_out_slice(ex_slice1) - assert joutslice.is_converged + assert joutslice.converged def test_jdftxoutfileslice_trajectory(): @@ -35,7 +35,7 @@ def test_jdftxoutfileslice_trajectory(): assert isinstance(traj, Trajectory) del joutslice.jsettings_lattice.params["niterations"] with pytest.raises(ValueError, match=re.escape("Unknown issue due to partial initialization of settings objects.")): - traj = joutslice.trajectory + joutslice._set_trajectory() def test_get_broadeningvars(): @@ -81,12 +81,18 @@ def test_get_eigstats_varsdict(): evardict = joutslice._get_eigstats_varsdict([], "$VAR") for key in evardict: assert evardict[key] is None + # Initializing eigvars with no data will set all to None EXCEPT efermi which has "mu" as a backup reference joutslice._set_eigvars([]) for key in evardict: if key != "efermi": assert getattr(joutslice, key) is None - for key in ["efermi", "mu"]: - assert getattr(joutslice, key) is not None + # Setting mu, _mu_backup, and jstrucs to None before _set_eigvars([]) will set efermi to None + joutslice.mu = None + joutslice._mu_backup = None + joutslice.jstrucs = None + joutslice._set_eigvars([]) + for key in evardict: + assert getattr(joutslice, key) is None def test_get_pp_type(): @@ -98,7 +104,7 @@ def test_get_pp_type(): def test_set_pseudo_vars_t1(): joutslice = JDFTXOutfileSlice._from_out_slice(ex_slice1) - # Just need more bound sets than there are atom types + # 6 instances of "reading pseudopotential file" since all possible test files have less than 6 atom types text = [ "Reading pseudopotential file not_SG15/GBRV", "10 valence electrons ", @@ -119,8 +125,7 @@ def test_set_pseudo_vars_t1(): "10 valence electrons ", "", ] - joutslice._total_electrons_backup = None - joutslice.jstrucs = None + joutslice.total_electrons = None with pytest.raises(ValueError, match="Total electrons and semicore electrons must be set."): joutslice._set_pseudo_vars_t1(text) joutslice.atom_elements = None From 0a229e438ee6a34f7a2481030d7331fb8f50bc8d Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 19:32:19 -0700 Subject: [PATCH 3/8] removing review requests for files with anticipated changes coming up --- src/pymatgen/io/jdftx/jdftxoutfileslice.py | 2 -- src/pymatgen/io/jdftx/jelstep.py | 2 -- src/pymatgen/io/jdftx/joutstructure.py | 2 -- src/pymatgen/io/jdftx/joutstructures.py | 2 -- src/pymatgen/io/jdftx/outputs.py | 2 -- 5 files changed, 10 deletions(-) diff --git a/src/pymatgen/io/jdftx/jdftxoutfileslice.py b/src/pymatgen/io/jdftx/jdftxoutfileslice.py index cafbe04dd0b..7820ff84f7e 100644 --- a/src/pymatgen/io/jdftx/jdftxoutfileslice.py +++ b/src/pymatgen/io/jdftx/jdftxoutfileslice.py @@ -2,8 +2,6 @@ This module defines the JDFTxOutfileSlice class, which is used to read and process a JDFTx out file. - -@mkhorton - This file is ready to review """ from __future__ import annotations diff --git a/src/pymatgen/io/jdftx/jelstep.py b/src/pymatgen/io/jdftx/jelstep.py index 3ce545adbf7..8a3b896c034 100644 --- a/src/pymatgen/io/jdftx/jelstep.py +++ b/src/pymatgen/io/jdftx/jelstep.py @@ -1,8 +1,6 @@ """Module for parsing single SCF step from JDFTx. This module contains the JElStep class for parsing single SCF step from a JDFTx out file. - -@mkhorton - this file is ready to review. """ from __future__ import annotations diff --git a/src/pymatgen/io/jdftx/joutstructure.py b/src/pymatgen/io/jdftx/joutstructure.py index 5bfe50ac0e4..365a5c7b344 100644 --- a/src/pymatgen/io/jdftx/joutstructure.py +++ b/src/pymatgen/io/jdftx/joutstructure.py @@ -1,8 +1,6 @@ """Class object for storing a single JDFTx geometric optimization step. A mutant of the pymatgen Structure class for flexibility in holding JDFTx. - -@mkhorton - this file is ready to review. """ from __future__ import annotations diff --git a/src/pymatgen/io/jdftx/joutstructures.py b/src/pymatgen/io/jdftx/joutstructures.py index dd6881ecace..b7bd4a4c7b5 100644 --- a/src/pymatgen/io/jdftx/joutstructures.py +++ b/src/pymatgen/io/jdftx/joutstructures.py @@ -2,8 +2,6 @@ This module contains the JOutStructures class for storing a series of JOutStructure. - -@mkhorton - this file is ready to review. """ from __future__ import annotations diff --git a/src/pymatgen/io/jdftx/outputs.py b/src/pymatgen/io/jdftx/outputs.py index e2d54c9af88..41680e4053f 100644 --- a/src/pymatgen/io/jdftx/outputs.py +++ b/src/pymatgen/io/jdftx/outputs.py @@ -4,8 +4,6 @@ Note: JDFTXOutfile will be moved back to its own module once a more broad outputs class is written. - -@mkhorton - this file is ready to review """ from __future__ import annotations From 0d99812a23a40c26b61bd2b68f1dca52b3a435e8 Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 20:29:59 -0700 Subject: [PATCH 4/8] replaced all properties within JElStep(s) and JOutStructure as initialized attributes (skipped for charges and magnetic_moments as the setter/getter methods are important as the user might interact with this object) --- src/pymatgen/io/jdftx/jelstep.py | 346 ++++++++----------------- src/pymatgen/io/jdftx/joutstructure.py | 197 ++++++-------- 2 files changed, 182 insertions(+), 361 deletions(-) diff --git a/src/pymatgen/io/jdftx/jelstep.py b/src/pymatgen/io/jdftx/jelstep.py index 8a3b896c034..f110acf44e5 100644 --- a/src/pymatgen/io/jdftx/jelstep.py +++ b/src/pymatgen/io/jdftx/jelstep.py @@ -5,11 +5,10 @@ from __future__ import annotations -import inspect import pprint import warnings from dataclasses import dataclass, field -from typing import Any, ClassVar +from typing import Any from pymatgen.core.units import Ha_to_eV from pymatgen.io.jdftx._output_utils import get_colon_var_t1 @@ -35,7 +34,7 @@ class JElStep: alpha (float | None): The step length. linmin (float | None): Normalized line minimization direction / energy gradient projection (-1 for perfectly opposite, 1 for perfectly aligned). - t_s (float | None): Time in seconds for the SCF step. + t_s (float | None): Time elapsed from beginning of JDFTx calculation. mu (float | None): The chemical potential in eV. nelectrons (float | None): The number of electrons. abs_magneticmoment (float | None): The absolute magnetic moment. @@ -241,25 +240,40 @@ class JElSteps: converged (bool): True if the SCF steps converged. converged_reason (str | None): The reason for convergence. slices (list[JElStep]): A list of JElStep objects. + e (float | None): The total electronic energy in eV. + grad_k (float | None): The gradient of the Kohn-Sham energy (along the + line minimization direction). + alpha (float | None): The step length. + linmin (float | None): Normalized line minimization direction / energy + gradient projection (-1 for perfectly opposite, 1 for perfectly aligned). + t_s (float | None): Time elapsed from beginning of JDFTx calculation. + mu (float | None): The chemical potential in eV. + nelectrons (float | None): The number of electrons. + abs_magneticmoment (float | None): The absolute magnetic moment. + tot_magneticmoment (float | None): The total magnetic moment. + subspacerotationadjust (float | None): The subspace rotation adjustment factor. + nstep (int | None): The SCF step number. """ opt_type: str | None = None etype: str | None = None iter_flag: str | None = None - converged: bool = False - converged_reason: str | None = None - slices: list[JElStep] = field(default_factory=list) - # List of attributes to ignore when getting attributes from the most recent slice specified by _getatr_ignore - _getatr_ignore: ClassVar[list[str]] = [ - "e", - "t_s", - "mu", - "nelectrons", - "subspacerotationadjust", - ] - - @property - def nstep(self) -> int | None: + converged: bool | None = field(default=None, init=True) + converged_reason: str | None = field(default=None, init=True) + slices: list[JElStep] = field(default_factory=list, init=True) + e: float | None = field(default=None, init=False) + grad_k: float | None = field(default=None, init=False) + alpha: float | None = field(default=None, init=False) + linmin: float | None = field(default=None, init=False) + t_s: float | None = field(default=None, init=False) + mu: float | None = field(default=None, init=False) + nelectrons: float | None = field(default=None, init=False) + abs_magneticmoment: float | None = field(default=None, init=False) + tot_magneticmoment: float | None = field(default=None, init=False) + subspacerotationadjust: float | None = field(default=None, init=False) + nstep: int | None = field(default=None, init=False) + + def _get_nstep(self) -> int | None: """Return the nstep attribute of the last JElStep object in the slices. The nstep attribute signifies the SCF step number. @@ -273,150 +287,13 @@ def nstep(self) -> int | None: """ if len(self.slices): if self.slices[-1].nstep is not None: - return self.slices[-1].nstep - warnings.warn("No nstep attribute in JElStep object. Returning number of JElStep objects.", stacklevel=2) - return len(self.slices) - 1 - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def e(self) -> float | None: - """Return total electronic energy. - - Return the e attribute of the last JElStep object in the slices, where e - signifies the total electronic energy in eV. - - Returns: - float: The e attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].e - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def grad_k(self) -> float | None: - """Return most recent grad_k. - - Return the grad_k attribute of the last JElStep object in the slices, where - grad_k signifies the gradient of the Kohn-Sham energy (along line minimization direction). - - Returns: - float: The grad_k attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].grad_k - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def alpha(self) -> float | None: - """Return most recent alpha. - - Return the alpha attribute of the last JElStep object in the slices, where - alpha signifies the step length in the electronic minimization. - - Returns: - float: The alpha attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].alpha - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def linmin(self) -> float | None: - """Return most recent linmin. - - Return the linmin attribute of the last JElStep object in the slices, where - linmin signifies the normalized line minimization direction / energy gradient projection. - - Returns: - float: The linmin attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].linmin - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def t_s(self) -> float | None: - """Return most recent t_s. - - Return the t_s attribute of the last JElStep object in the slices, where - t_s signifies the time in seconds for the SCF step. - - Returns: - float: The t_s attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].t_s - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def mu(self) -> float | None: - """Return most recent mu. - - Return the mu attribute of the last JElStep object in the slices, where - mu signifies the chemical potential (Fermi level) in eV. - - Returns: - float: The mu attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].mu - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def nelectrons(self) -> float | None: - """Return most recent nelectrons. - - Return the nelectrons attribute of the last JElStep object in the slices, where - nelectrons signifies the total number of electrons being evaluated in the SCF step. - - Returns: - float: The nelectrons attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].nelectrons - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def abs_magneticmoment(self) -> float | None: - """Return most recent abs_magneticmoment. - - Return the abs_magneticmoment attribute of the last JElStep object in the slices, where - abs_magneticmoment signifies the absolute magnetic moment of the electron density. - - Returns: - float: The abs_magneticmoment attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].abs_magneticmoment - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def tot_magneticmoment(self) -> float | None: - """ - Return most recent tot_magneticmoment. - - Return the tot_magneticmoment attribute of the last JElStep object in the slices, where - tot_magneticmoment signifies the total magnetic moment of the electron density. - - Returns: - float: The tot_magneticmoment attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].tot_magneticmoment - raise AttributeError("No JElStep objects in JElSteps object slices class variable.") - - @property - def subspacerotationadjust(self) -> float | None: - """Return most recent subspacerotationadjust. - - Return the subspacerotationadjust attribute of the last JElStep object in the slices, where - subspacerotationadjust signifies the amount by which the subspace was rotated in the SCF step. - - Returns: - float: The subspacerotationadjust attribute of the last JElStep object in the slices. - """ - if len(self.slices): - return self.slices[-1].subspacerotationadjust + nstep = self.slices[-1].nstep + else: + warnings.warn( + "No nstep attribute in JElStep object. Returning number of JElStep objects.", stacklevel=2 + ) + nstep = len(self.slices) - 1 + return nstep raise AttributeError("No JElStep objects in JElSteps object slices class variable.") @classmethod @@ -432,88 +309,32 @@ def _from_text_slice(cls, text_slice: list[str], opt_type: str = "ElecMinimize", etype (str): The type of energy component. """ line_collections, lines_collect = _gather_JElSteps_line_collections(opt_type, text_slice) - instance = cls() - instance.iter_flag = f"{opt_type}: Iter:" - instance.opt_type = opt_type - instance.etype = etype - instance.slices = [] + slices = [] + converged = None + converged_reason = None for _lines_collect in line_collections: - instance.slices.append(JElStep._from_lines_collect(_lines_collect, opt_type, etype)) + slices.append(JElStep._from_lines_collect(_lines_collect, opt_type, etype)) if len(lines_collect): - instance._parse_ending_lines(lines_collect) - lines_collect = [] + converged, converged_reason = _parse_ending_lines(lines_collect, opt_type) + instance = cls(slices=slices, converged=converged, converged_reason=converged_reason) + instance.opt_type = opt_type + instance.etype = etype return instance - def _parse_ending_lines(self, ending_lines: list[str]) -> None: - """Parse ending lines. - - Parses the ending lines of text from a JDFTx out file corresponding to - a series of SCF steps. - - Args: - ending_lines (list[str]): The ending lines of text from a JDFTx out file corresponding to a - series of SCF steps. - """ - for i, line in enumerate(ending_lines): - if self._is_converged_line(i, line): - self._read_converged_line(line) - - def _is_converged_line(self, i: int, line_text: str) -> bool: - """Return True if converged line. - - Return True if the line_text is the start of a log message about - convergence for a JDFTx optimization step. - - Args: - i (int): The index of the line in the text slice. - line_text (str): A line of text from a JDFTx out file. - - Returns: - bool: True if the line_text is the start of a log message about - convergence for a JDFTx optimization step. - """ - return f"{self.opt_type}: Converged" in line_text - - def _read_converged_line(self, line_text: str) -> None: - """Set class variables converged and converged_reason. - - Args: - line_text (str): A line of text from a JDFTx out file containing a message about - convergence for a JDFTx optimization step. - """ - self.converged = True - self.converged_reason = line_text.split("(")[1].split(")")[0].strip() - - # This method is likely never going to be called as all (currently existing) - # attributes of the most recent slice are explicitly defined as a class - # property. However, it is included to reduce the likelihood of errors - # upon future changes to downstream code. - def __getattr__(self, name: str) -> Any: - """Return attribute value. - - Args: - name (str): The name of the attribute. - - Returns: - Any: The value of the attribute. - - Raises: - AttributeError: If the attribute is not found. - """ - if name in self.__dict__: - return self.__dict__[name] - - # Check if the attribute is a property of the class - for cls in inspect.getmro(self.__class__): - if name in cls.__dict__ and isinstance(cls.__dict__[name], property): - return cls.__dict__[name].__get__(self) - - # Check if the attribute is in self.jstrucs - if hasattr(self.slices[-1], name): - return getattr(self.slices[-1], name) - - # If the attribute is not found in either, raise an AttributeError - raise AttributeError(f"{self.__class__.__name__} not found: {name}") + def __post_init__(self) -> None: + """Post initialization method.""" + if len(self.slices): + self.e = self.slices[-1].e + self.grad_k = self.slices[-1].grad_k + self.alpha = self.slices[-1].alpha + self.linmin = self.slices[-1].linmin + self.t_s = self.slices[-1].t_s + self.mu = self.slices[-1].mu + self.nelectrons = self.slices[-1].nelectrons + self.abs_magneticmoment = self.slices[-1].abs_magneticmoment + self.tot_magneticmoment = self.slices[-1].tot_magneticmoment + self.subspacerotationadjust = self.slices[-1].subspacerotationadjust + self.nstep = self._get_nstep() def __getitem__(self, key: int | str) -> JElStep | Any: """Return item. @@ -623,3 +444,50 @@ def _gather_JElSteps_line_collections(opt_type: str, text_slice: list[str]) -> t else: break return line_collections, lines_collect + + +def _parse_ending_lines(ending_lines: list[str], opt_type: str) -> tuple[None | bool, None | str]: + """Parse ending lines. + + Parses the ending lines of text from a JDFTx out file corresponding to + a series of SCF steps. + + Args: + ending_lines (list[str]): The ending lines of text from a JDFTx out file corresponding to a + series of SCF steps. + """ + converged = None + converged_reason = None + for i, line in enumerate(ending_lines): + if _is_converged_line(i, line, opt_type): + converged, converged_reason = _read_converged_line(line) + return converged, converged_reason + + +def _is_converged_line(i: int, line_text: str, opt_type: str) -> bool: + """Return True if converged line. + + Return True if the line_text is the start of a log message about + convergence for a JDFTx optimization step. + + Args: + i (int): The index of the line in the text slice. + line_text (str): A line of text from a JDFTx out file. + + Returns: + bool: True if the line_text is the start of a log message about + convergence for a JDFTx optimization step. + """ + return f"{opt_type}: Converged" in line_text + + +def _read_converged_line(line_text: str) -> tuple[None | bool, None | str]: + """Set class variables converged and converged_reason. + + Args: + line_text (str): A line of text from a JDFTx out file containing a message about + convergence for a JDFTx optimization step. + """ + converged = True + converged_reason = line_text.split("(")[1].split(")")[0].strip() + return converged, converged_reason diff --git a/src/pymatgen/io/jdftx/joutstructure.py b/src/pymatgen/io/jdftx/joutstructure.py index 365a5c7b344..750ff7d739c 100644 --- a/src/pymatgen/io/jdftx/joutstructure.py +++ b/src/pymatgen/io/jdftx/joutstructure.py @@ -29,6 +29,39 @@ class JOutStructure(Structure): A mutant of the pymatgen Structure class for flexibility in holding JDFTx optimization data. + + Properties: + charges (np.ndarray | None): The Lowdin charges of the atoms in the system. + magnetic_moments (np.ndarray | None): The magnetic moments of the atoms in the system. + Attributes: + opt_type (str | None): The type of optimization step. + etype (str | None): The type of energy from the electronic minimization data. + eopt_type (str | None): The type of electronic minimization step. + emin_flag (str | None): The flag that indicates the start of a log message for a JDFTx optimization step. + ecomponents (dict | None): The energy components of the system. + elecmindata (JElSteps | None): The electronic minimization data. + stress (np.ndarray | None): The stress tensor. + strain (np.ndarray | None): The strain tensor. + nstep (int | None): The most recent step number. + e (float | None): The total energy of the system. + grad_k (float | None): The gradient of the electronic density along the most recent line minimization. + alpha (float | None): The step size of the most recent SCF step along the line minimization. + linmin (float | None): The normalized alignment projection of the electronic energy gradient to the line + minimization direction. + t_s (float | None): The time in seconds for the optimization step. + geom_converged (bool): Whether the geometry optimization has converged. + geom_converged_reason (str | None): The reason for geometry optimization convergence. + line_types (ClassVar[list[str]]): The types of lines in a JDFTx out file. + selective_dynamics (list[int] | None): The selective dynamics flags for the atoms in the system. + mu (float | None): The chemical potential (Fermi level) in eV. + nelectrons (float | None): The total number of electrons in the electron density. + abs_magneticmoment (float | None): The absolute magnetic moment of the electron density. + tot_magneticmoment (float | None): The total magnetic moment of the electron density. + elec_nstep (int | None): The most recent electronic step number. + elec_e (float | None): The most recent electronic energy. + elec_grad_k (float | None): The most recent electronic grad_k. + elec_alpha (float | None): The most recent electronic alpha. + elec_linmin (float | None): The most recent electronic linmin. """ opt_type: str | None = None @@ -59,111 +92,28 @@ class JOutStructure(Structure): "opt", ] selective_dynamics: list[int] | None = None - - @property - def mu(self) -> float | None: - """Return the chemical potential. - - Returns: - float: The chemical potential (Fermi level) in eV. - """ - if self.elecmindata is not None: - return self.elecmindata.mu - return None - - @property - def nelectrons(self) -> float | None: - """Return the number of electrons. - - Returns: - float: The total number of electrons in the electron density. - """ - if self.elecmindata is not None: - return self.elecmindata.nelectrons - return None - - @property - def abs_magneticmoment(self) -> float | None: - """Return the absolute magnetic moment. - - Returns: - float: The absolute magnetic moment of the electron density. - """ - if self.elecmindata is not None: - return self.elecmindata.abs_magneticmoment - return None - - @property - def tot_magneticmoment(self) -> float | None: - """Return the total magnetic moment. - - Returns: - float: The total magnetic moment of the electron density. - """ - if self.elecmindata is not None: - return self.elecmindata.tot_magneticmoment - return None - - @property - def elec_nstep(self) -> int | None: - """Return the most recent electronic step number. - - Returns: - int: The nstep property of the electronic minimization data, where - nstep corresponds to the SCF step number. - """ - if self.elecmindata is not None: - return self.elecmindata.nstep - return None - - @property - def elec_e(self) -> int | None: - """Return the most recent electronic energy. - - Returns: - float: The e property of the electronic minimization, where e corresponds - to the energy of the system's "etype". - """ - if self.elecmindata is not None: - return self.elecmindata.e - return None - - @property - def elec_grad_k(self) -> float | None: - """Return the most recent electronic grad_k. - - Returns: - float: The most recent electronic grad_k, where grad_k here corresponds - to the gradient of the electronic density along the most recent line minimization. - """ + mu: float | None = None + nelectrons: float | None = None + abs_magneticmoment: float | None = None + tot_magneticmoment: float | None = None + elec_nstep: int | None = None + elec_e: float | None = None + elec_grad_k: float | None = None + elec_alpha: float | None = None + elec_linmin: float | None = None + + def _elecmindata_postinit(self) -> None: + """Post-initialization method for attributes taken from elecmindata.""" if self.elecmindata is not None: - return self.elecmindata.grad_k - return None - - @property - def elec_alpha(self) -> float | None: - """Return the most recent electronic alpha. - - Returns: - float: The most recent electronic alpha, where alpha here corresponds to the - step size of the most recent SCF step along the line minimization. - """ - if self.elecmindata is not None: - return self.elecmindata.alpha - return None - - @property - def elec_linmin(self) -> float | None: - """Return the most recent electronic linmin. - - Returns: - float: The most recent electronic linmin, where linmin here corresponds to - the normalized alignment projection of the electronic energy gradient to - the line minimization direction. (-1 perfectly anti-aligned, 0 orthogonal, 1 perfectly aligned) - """ - if self.elecmindata is not None: - return self.elecmindata.linmin - return None + self.mu = self.elecmindata.mu + self.nelectrons = self.elecmindata.nelectrons + self.abs_magneticmoment = self.elecmindata.abs_magneticmoment + self.tot_magneticmoment = self.elecmindata.tot_magneticmoment + self.elec_nstep = self.elecmindata.nstep + self.elec_e = self.elecmindata.e + self.elec_grad_k = self.elecmindata.grad_k + self.elec_alpha = self.elecmindata.alpha + self.elec_linmin = self.elecmindata.linmin @property def charges(self) -> np.ndarray | None: @@ -268,30 +218,35 @@ def _from_text_slice( line_collections = instance._init_line_collections() line_collections = instance._gather_line_collections(line_collections, text_slice) - # ecomponents needs to be parsed before emin to set etype + # ecomponents needs to be parsed before emin and opt to set etype instance._parse_ecomp_lines(line_collections["ecomp"]["lines"]) + instance._parse_opt_lines(line_collections["opt"]["lines"]) instance._parse_emin_lines(line_collections["emin"]["lines"]) - # Lattice must be parsed before posns/forces in case of direct - # coordinates + # Lattice must be parsed before posns/forces in case of direct coordinates instance._parse_lattice_lines(line_collections["lattice"]["lines"]) - instance._parse_posns_lines(line_collections["posns"]["lines"]) instance._parse_forces_lines(line_collections["forces"]["lines"]) - # Strain and stress can be parsed in any order - instance._parse_strain_lines(line_collections["strain"]["lines"]) - instance._parse_stress_lines(line_collections["stress"]["lines"]) + instance._parse_posns_lines(line_collections["posns"]["lines"]) # Lowdin must be parsed after posns instance._parse_lowdin_lines(line_collections["lowdin"]["lines"]) - # Opt line must be parsed after ecomp - instance._parse_opt_lines(line_collections["opt"]["lines"]) + # Strain and stress can be parsed at any point + instance._parse_strain_lines(line_collections["strain"]["lines"]) + instance._parse_stress_lines(line_collections["stress"]["lines"]) # In case of single-point calculation - if instance.e is None: # This doesn't defer to elecmindata.e due to the existence of a class variable e - if instance.etype is not None: - if instance.ecomponents is not None: - if instance.etype in instance.ecomponents: - instance.e = instance.ecomponents[instance.etype] - elif instance.elecmindata is not None: - instance.e = instance.elecmindata.e + instance._init_e_sp_backup() + # Setting attributes from elecmindata (set during _parse_emin_lines) + instance._elecmindata_postinit() + return instance + + def _init_e_sp_backup(self) -> None: + """Initialize self.e with coverage for single-point calculations.""" + if self.e is None: # This doesn't defer to elecmindata.e due to the existence of a class variable e + if self.etype is not None: + if self.ecomponents is not None: + if self.etype in self.ecomponents: + self.e = self.ecomponents[self.etype] + elif self.elecmindata is not None: + self.e = self.elecmindata.e else: raise ValueError("Could not determine total energy due to lack of elecmindata") else: @@ -299,8 +254,6 @@ def _from_text_slice( else: raise ValueError("Could not determine total energy due to lack of etype") - return instance - def _init_line_collections(self) -> dict: """Initialize line collection dict. From 8f8cfd269d88efbbd955e0ccd871120666c63511 Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 21:31:44 -0700 Subject: [PATCH 5/8] Cleaning up __post_init__ methods to iterate over a list of variables for more easier reading --- src/pymatgen/io/jdftx/jdftxoutfileslice.py | 2 +- src/pymatgen/io/jdftx/jelstep.py | 26 +- src/pymatgen/io/jdftx/joutstructure.py | 18 +- src/pymatgen/io/jdftx/joutstructures.py | 741 +++++++-------------- src/pymatgen/io/jdftx/outputs.py | 156 ++--- 5 files changed, 364 insertions(+), 579 deletions(-) diff --git a/src/pymatgen/io/jdftx/jdftxoutfileslice.py b/src/pymatgen/io/jdftx/jdftxoutfileslice.py index 7820ff84f7e..cba8dd50571 100644 --- a/src/pymatgen/io/jdftx/jdftxoutfileslice.py +++ b/src/pymatgen/io/jdftx/jdftxoutfileslice.py @@ -863,6 +863,7 @@ def _set_jstrucs(self, text: list[str]) -> None: self.etype = self.jstrucs[-1].etype if self.jstrucs is not None: self._set_trajectory() + self.mu = self._get_mu() self.structure = self.jstrucs[-1] self.eopt_type = self.jstrucs.eopt_type self.elecmindata = self.jstrucs.elecmindata @@ -875,7 +876,6 @@ def _set_jstrucs(self, text: list[str]) -> None: self.linmin = self.jstrucs.linmin self.abs_magneticmoment = self.jstrucs.abs_magneticmoment self.tot_magneticmoment = self.jstrucs.tot_magneticmoment - self.mu = self._get_mu() self.elec_nstep = self.jstrucs.elec_nstep self.elec_e = self.jstrucs.elec_e self.elec_grad_k = self.jstrucs.elec_grad_k diff --git a/src/pymatgen/io/jdftx/jelstep.py b/src/pymatgen/io/jdftx/jelstep.py index f110acf44e5..f56a50c7d98 100644 --- a/src/pymatgen/io/jdftx/jelstep.py +++ b/src/pymatgen/io/jdftx/jelstep.py @@ -226,6 +226,20 @@ def __str__(self) -> str: return pprint.pformat(self) +_jelsteps_atrs_from_last_slice = [ + "e", + "grad_k", + "alpha", + "linmin", + "t_s", + "mu", + "nelectrons", + "abs_magneticmoment", + "tot_magneticmoment", + "subspacerotationadjust", +] + + @dataclass class JElSteps: """Class object for series of SCF steps. @@ -324,17 +338,9 @@ def _from_text_slice(cls, text_slice: list[str], opt_type: str = "ElecMinimize", def __post_init__(self) -> None: """Post initialization method.""" if len(self.slices): - self.e = self.slices[-1].e - self.grad_k = self.slices[-1].grad_k - self.alpha = self.slices[-1].alpha - self.linmin = self.slices[-1].linmin - self.t_s = self.slices[-1].t_s - self.mu = self.slices[-1].mu - self.nelectrons = self.slices[-1].nelectrons - self.abs_magneticmoment = self.slices[-1].abs_magneticmoment - self.tot_magneticmoment = self.slices[-1].tot_magneticmoment - self.subspacerotationadjust = self.slices[-1].subspacerotationadjust self.nstep = self._get_nstep() + for var in _jelsteps_atrs_from_last_slice: + setattr(self, var, getattr(self.slices[-1], var)) def __getitem__(self, key: int | str) -> JElStep | Any: """Return item. diff --git a/src/pymatgen/io/jdftx/joutstructure.py b/src/pymatgen/io/jdftx/joutstructure.py index 750ff7d739c..41477e46625 100644 --- a/src/pymatgen/io/jdftx/joutstructure.py +++ b/src/pymatgen/io/jdftx/joutstructure.py @@ -23,6 +23,9 @@ __author__ = "Ben Rich" +_jos_atrs_from_elecmindata = ["mu", "nelectrons", "abs_magneticmoment", "tot_magneticmoment"] +_jos_atrs_elec_from_elecmindata = ["nstep", "e", "grad_k", "alpha", "linmin"] + class JOutStructure(Structure): """Class object for storing a single JDFTx optimization step. @@ -105,15 +108,12 @@ class JOutStructure(Structure): def _elecmindata_postinit(self) -> None: """Post-initialization method for attributes taken from elecmindata.""" if self.elecmindata is not None: - self.mu = self.elecmindata.mu - self.nelectrons = self.elecmindata.nelectrons - self.abs_magneticmoment = self.elecmindata.abs_magneticmoment - self.tot_magneticmoment = self.elecmindata.tot_magneticmoment - self.elec_nstep = self.elecmindata.nstep - self.elec_e = self.elecmindata.e - self.elec_grad_k = self.elecmindata.grad_k - self.elec_alpha = self.elecmindata.alpha - self.elec_linmin = self.elecmindata.linmin + for var in _jos_atrs_from_elecmindata: + if hasattr(self.elecmindata, var): + setattr(self, var, getattr(self.elecmindata, var)) + for var in _jos_atrs_elec_from_elecmindata: + if hasattr(self.elecmindata, var): + setattr(self, f"elec_{var}", getattr(self.elecmindata, var)) @property def charges(self) -> np.ndarray | None: diff --git a/src/pymatgen/io/jdftx/joutstructures.py b/src/pymatgen/io/jdftx/joutstructures.py index b7bd4a4c7b5..b060efec7dd 100644 --- a/src/pymatgen/io/jdftx/joutstructures.py +++ b/src/pymatgen/io/jdftx/joutstructures.py @@ -6,24 +6,49 @@ from __future__ import annotations -import inspect import pprint from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any +if TYPE_CHECKING: + from pymatgen.io.jdftx.jelstep import JElSteps + import numpy as np from pymatgen.core.structure import Structure from pymatgen.core.units import bohr_to_ang -from pymatgen.io.jdftx._output_utils import correct_geom_opt_type, is_lowdin_start_line - -if TYPE_CHECKING: - from pymatgen.io.jdftx.jelstep import JElSteps -from pymatgen.io.jdftx._output_utils import find_first_range_key +from pymatgen.io.jdftx._output_utils import correct_geom_opt_type, find_first_range_key, is_lowdin_start_line from pymatgen.io.jdftx.joutstructure import JOutStructure __author__ = "Ben Rich" +_joss_atrs_from_last_slice = [ + "etype", + "eopt_type", + "emin_flag", + "ecomponents", + "elecmindata", + "stress", + "strain", + "nstep", + "e", + "grad_k", + "alpha", + "linmin", + "nelectrons", + "abs_magneticmoment", + "tot_magneticmoment", + "mu", + "elec_nstep", + "elec_e", + "elec_grad_k", + "elec_alpha", + "elec_linmin", + "charges", + "magnetic_moments", + "selective_dynamics", +] + @dataclass class JOutStructures: @@ -35,6 +60,43 @@ class JOutStructures: Attributes: out_slice_start_flag (str): The string that marks the beginning of the portion of an out file slice that contains data for a JOutStructures object. + opt_type (str | None): The type of optimization performed on the structures in the JOutStructures object. + geom_converged (bool): Whether the geometry of the last structure in the list has converged. + geom_converged_reason (str | None): The reason the geometry of the last structure in the list has converged. + elec_converged (bool): Whether the electronic density of the last structure in the list has converged. + elec_converged_reason (str | None): The reason the electronic density of the last structure in the list has + converged. + slices (list[JOutStructure]): A list of JOutStructure objects. + eopt_type (str | None): The type of electronic optimization performed on the last structure in the list. + etype (str | None): String representation of total energy-type of system. Commonly "G" + (grand-canonical potential) for GC calculations, and "F" for canonical (fixed electron count) calculations. + emin_flag (str | None): The flag for the electronic minimization. + ecomponents (list[str] | None): The components of the electronic minimization. + elecmindata (JElSteps): The electronic minimization data. + stress (np.ndarray | None): The stress tensor. + strain (np.ndarray | None): The strain tensor. + nstep (int | None): The number of steps in the optimization. + e (float | None): The total energy. + grad_k (float | None): The final norm of the preconditioned gradient for geometric optimization of the most + recent JDFTx call (evaluated as dot(g, Kg), where g is the gradient and Kg is the preconditioned gradient). + (written as "|grad|_K" in JDFTx output). + alpha (float | None): The step size of the final geometric step in the most recent JDFTx call. + linmin (float | None): The final normalized projection of the geometric step direction onto the gradient for + the most recent JDFTx call. + abs_magneticmoment (float | None): The absolute magnetic moment of the most recent JDFTx call. + tot_magneticmoment (float | None): The total magnetic moment of the most recent JDFTx call. + mu (float | None): The Fermi energy of the most recent JDFTx call. + elec_e (float) | None: The final energy of the most recent electronic optimization step. + elec_nstep (int): The number of electronic optimization steps in the most recent JDFTx call. + elec_grad_k (float | None): The final norm of the preconditioned gradient for electronic optimization of the + most recent JDFTx call (evaluated as dot(g, Kg), where g is the gradient and Kg is the preconditioned + gradient). (written as "|grad|_K" in JDFTx output). + elec_alpha (float) | None: The step size of the final electronic step in the most recent JDFTx call. + elec_linmin (float | None): The final normalized projection of the electronic step direction onto the gradient + for the most recent JDFTx call. + charges (np.ndarray[float] | None): The most recent Lowdin-charges. + magnetic_moments (np.ndarray[float] | None): The most recent Lowdin-magnetic moments. + selective_dynamics (list[int] | None): The selective dynamics flags for the most recent JDFTx call. """ out_slice_start_flag = "-------- Electronic minimization -----------" @@ -44,7 +106,30 @@ class JOutStructures: elec_converged: bool = False elec_converged_reason: str | None = None _t_s: float | None = None - slices: list[JOutStructure] = field(default_factory=list) + slices: list[JOutStructure] = field(default_factory=list, init=True) + eopt_type: str | None = None + etype: str | None = None + emin_flag: str | None = None + ecomponents: list[str] | None = None + elecmindata: JElSteps = None + stress: np.ndarray | None = None + strain: np.ndarray | None = None + nstep: int | None = None + e: float | None = None + grad_k: float | None = None + alpha: float | None = None + linmin: float | None = None + nelectrons: float | None = None + abs_magneticmoment: float | None = None + tot_magneticmoment: float | None = None + mu: float | None = None + elec_nstep: int | None = None + elec_e: float | None = None + elec_grad_k: float | None = None + elec_alpha: float | None = None + elec_linmin: float | None = None + charges: np.ndarray[float] | None = None + magnetic_moments: np.ndarray[float] | None = None @classmethod def _from_out_slice(cls, out_slice: list[str], opt_type: str = "IonicMinimize") -> JOutStructures: @@ -60,22 +145,23 @@ def _from_out_slice(cls, out_slice: list[str], opt_type: str = "IonicMinimize") Returns: JOutStructures: The created JOutStructures object. """ - instance = cls() if opt_type not in ["IonicMinimize", "LatticeMinimize"]: opt_type = correct_geom_opt_type(opt_type) - instance.opt_type = opt_type start_idx = _get_joutstructures_start_idx(out_slice) - init_struc = instance._get_init_structure(out_slice[:start_idx]) - instance._set_joutstructure_list(out_slice[start_idx:], init_structure=init_struc) - if instance.opt_type is None and len(instance) > 1: + init_struc = _get_init_structure(out_slice[:start_idx]) + slices = _get_joutstructure_list(out_slice[start_idx:], opt_type, init_structure=init_struc) + return cls(slices=slices) + + def __post_init__(self): + self.opt_type = self.slices[-1].opt_type + if self.opt_type is None and len(self) > 1: raise Warning("iter type interpreted as single-point calculation, but multiple structures found") - instance._check_convergence() - return instance + self.t_s = self._get_t_s() + for var in _joss_atrs_from_last_slice: + setattr(self, var, getattr(self.slices[-1], var)) + self._check_convergence() - # TODO: This currently returns the most recent t_s, which is not at all helpful. - # Correct this to be the total time in seconds for the series of structures. - @property - def t_s(self) -> float | None: + def _get_t_s(self) -> float | None: """Return time of calculation. Returns: @@ -90,444 +176,6 @@ def t_s(self) -> float | None: self._t_s = self[-1].t_s return self._t_s - ########################################################################### - # Properties inherited from most recent JOutStructure - ########################################################################### - - @property - def etype(self) -> str | None: - """ - Return etype from most recent JOutStructure. - - Returns: - str | None: etype from most recent JOutStructure, where etype corresponds to the string - representation of the ensemble potential - (ie "F" for Helmholtz, "G" for Grand-Canonical Potential). - """ - if len(self.slices): - return self.slices[-1].etype - raise AttributeError("Property etype inaccessible due to empty slices class field") - - @property - def eopt_type(self) -> str | None: - """ - Return eopt_type from most recent JOutStructure. - - Returns: - str | None: eopt_type from most recent JOutStructure, where eopt_type corresponds to the - JDFTx string representation for the minimization program used to minimize the electron density. - """ - if len(self.slices): - return self.slices[-1].eopt_type - raise AttributeError("Property eopt_type inaccessible due to empty slices class field") - - @property - def emin_flag(self) -> str | None: - """ - Return emin_flag from most recent JOutStructure. - - Returns: - str | None: emin_flag from most recent JOutStructure, where emin_flag corresponds to the - flag string used to mark the beginning of a section of the out file containing the - data to construct a JOutStructure object. - """ - if len(self.slices): - return self.slices[-1].emin_flag - raise AttributeError("Property emin_flag inaccessible due to empty slices class field") - - @property - def ecomponents(self) -> dict | None: - """ - Return ecomponents from most recent JOutStructure. - - Returns: - dict | None: ecomponents from most recent JOutStructure, where ecomponents is a dictionary - mapping string representation of system energy types to their values in eV. - """ - if len(self.slices): - return self.slices[-1].ecomponents - raise AttributeError("Property ecomponents inaccessible due to empty slices class field") - - @property - def elecmindata(self) -> JElSteps | None: - """ - Return elecmindata from most recent JOutStructure. - - Returns: - JElSteps | None: elecmindata from most recent JOutStructure, where elecmindata is a JElSteps object - created to hold electronic minimization data on the electronic density for this JOutStructure. - """ - if len(self.slices): - return self.slices[-1].elecmindata - raise AttributeError("Property elecmindata inaccessible due to empty slices class field") - - # TODO: Figure out how JDFTx defines the equilibrium lattice parameters and - # incorporate into this docstring. - @property - def stress(self) -> np.ndarray | None: - """ - Return stress from most recent JOutStructure. - - Returns: - np.ndarray | None: stress from most recent JOutStructure, where stress is the 3x3 unitless - stress tensor. - """ - if len(self.slices): - return self.slices[-1].stress - raise AttributeError("Property stress inaccessible due to empty slices class field") - - @property - def strain(self) -> np.ndarray | None: - """ - Return strain from most recent JOutStructure. - - Returns: - np.ndarray | None: strain from most recent JOutStructure, where strain is the 3x3 strain - tensor in units eV/A^3. - """ - if len(self.slices): - return self.slices[-1].strain - raise AttributeError("Property strain inaccessible due to empty slices class field") - - @property - def nstep(self) -> int | None: - """ - Return nstep from most recent JOutStructure. - - Returns: - int | None: nstep from most recent JOutStructure, where nstep corresponds to the step - number of the geometric optimization. - """ - if len(self.slices): - return self.slices[-1].nstep - raise AttributeError("Property nstep inaccessible due to empty slices class field") - - @property - def e(self) -> float | None: - """ - Return e from most recent JOutStructure. - - Returns: - float | None: e from most recent JOutStructure, where e corresponds to the system energy - of the system's "etype" in eV. - """ - if len(self.slices): - return self.slices[-1].e - raise AttributeError("Property e inaccessible due to empty slices class field") - - @property - def grad_k(self) -> float | None: - """ - Return grad_k from most recent JOutStructure. - - Returns: - float | None: grad_k from most recent JOutStructure, where grad_k corresponds to the geometric - gradient along the geometric line minimization. - """ - if len(self.slices): - return self.slices[-1].grad_k - raise AttributeError("Property grad_k inaccessible due to empty slices class field") - - @property - def alpha(self) -> float | None: - """ - Return alpha from most recent JOutStructure. - - Returns: - float | None: alpha from most recent JOutStructure, where alpha corresponds to the geometric - step size along the geometric line minimization. - """ - if len(self.slices): - return self.slices[-1].alpha - raise AttributeError("Property alpha inaccessible due to empty slices class field") - - @property - def linmin(self) -> float | None: - """ - Return linmin from most recent JOutStructure. - - Returns: - float | None: linmin from most recent JOutStructure, where linmin corresponds to the normalized - projection of the geometric gradient to the step direction within the line minimization. - """ - if len(self.slices): - return self.slices[-1].linmin - raise AttributeError("Property linmin inaccessible due to empty slices class field") - - @property - def nelectrons(self) -> float | None: - """ - Return nelectrons from most recent JOutStructure. - - Returns: - float | None: nelectrons from most recent JOutStructure, where nelectrons corresponds to the - number of electrons in the electron density. - """ - if len(self.slices): - return self.slices[-1].nelectrons - raise AttributeError("Property nelectrons inaccessible due to empty slices class field") - - @property - def abs_magneticmoment(self) -> float | None: - """ - Return abs_magneticmoment from most recent JOutStructure. - - Returns: - float | None: abs_magneticmoment from most recent JOutStructure, where abs_magneticmoment corresponds - to the absolute magnetic moment of the electron density. - """ - if len(self.slices): - return self.slices[-1].abs_magneticmoment - raise AttributeError("Property abs_magneticmoment inaccessible due to empty slices class field") - - @property - def tot_magneticmoment(self) -> float | None: - """ - Return tot_magneticmoment from most recent JOutStructure. - - Returns: - float | None: tot_magneticmoment from most recent JOutStructure, where tot_magneticmoment corresponds - to the total magnetic moment of the electron density. - """ - if len(self.slices): - return self.slices[-1].tot_magneticmoment - raise AttributeError("Property tot_magneticmoment inaccessible due to empty slices class field") - - @property - def mu(self) -> float | None: - """ - Return mu from most recent JOutStructure. - - Returns: - float | None: mu from most recent JOutStructure, where mu corresponds to the electron chemical potential - (Fermi level) in eV. - """ - if len(self.slices): - return self.slices[-1].mu - raise AttributeError("Property mu inaccessible due to empty slices class field") - - ########################################################################### - # Electronic properties inherited from most recent JElSteps with symbol - # disambiguation. - ########################################################################### - - @property - def elec_nstep(self) -> int | None: - """Return the most recent electronic step number. - - Returns: - int: The most recent elec_nstep, where elec_nstep corresponds to the SCF step number. - """ - if len(self.slices): - return self.slices[-1].elec_nstep - raise AttributeError("Property elec_nstep inaccessible due to empty slices class field") - - @property - def elec_e(self) -> float | None: - """Return the most recent elec_e. - - Returns: - float: The most recent elec_e, where elec_e corresponds to the system's "etype" energy as printed - within the SCF log. - """ - if len(self.slices): - return self.slices[-1].elec_e - raise AttributeError("Property elec_e inaccessible due to empty slices class field") - - @property - def elec_grad_k(self) -> float | None: - """Return the most recent elec_grad_k. - - Returns: - float: The most recent elec_grad_k, where elec_grad_k corresponds to the electronic gradient along the - line minimization (equivalent to grad_k for a JElSteps object). - """ - if len(self.slices): - return self.slices[-1].elec_grad_k - raise AttributeError("Property grad_k inaccessible due to empty slices class field") - - @property - def elec_alpha(self) -> float | None: - """Return the most recent elec_alpha.d - - Returns: - float: The most recent elec_alpha, where elec_alpha corresponds to the step size of the electronic - optimization (equivalent to alpha for a JElSteps object). - """ - if len(self.slices): - return self.slices[-1].elec_alpha - raise AttributeError("Property alpha inaccessible due to empty slices class field") - - @property - def elec_linmin(self) -> float | None: - """Return the most recent elec_linmin. - - Returns: - float: The most recent elec_linmin, where elec_linmin corresponds to the normalized projection of the - electronic gradient on the electronic line minimization direction - (equivalent to linmin for a JElSteps object). - """ - if len(self.slices): - return self.slices[-1].elec_linmin - raise AttributeError("Property linmin inaccessible due to empty slices class field") - - def _get_init_structure(self, pre_out_slice: list[str]) -> Structure | None: - """ - Return initial structure. - - Return the initial structure from the pre_out_slice, corresponding to all data cut from JOutStructure list - initialization. This is needed to ensure structural data that is not being updated (and therefore not being - logged in the out file) is still available. - - Args: - pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that - contains the initial structure information. - - Returns: - Structure | None: The initial structure if available, otherwise None. - """ - try: - lat_mat = self._get_initial_lattice(pre_out_slice) - coords = self._get_initial_coords(pre_out_slice) - species = self._get_initial_species(pre_out_slice) - return Structure(lattice=lat_mat, species=species, coords=coords) - except AttributeError: - return None - - def _get_initial_lattice(self, pre_out_slice: list[str]) -> np.ndarray: - """Return initial lattice. - - Return the initial lattice from the pre_out_slice, corresponding to all data cut from JOutStructure list - initialization. This is needed to ensure lattice data that is not being updated (and therefore not being - logged in the out file) is still available. - - Args: - pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that - contains the initial lattice information. - - Returns: - np.ndarray: The initial lattice matrix. - """ - lat_lines = find_first_range_key("lattice ", pre_out_slice) - if len(lat_lines): - lat_line = lat_lines[0] - lat_mat = np.zeros([3, 3]) - for i in range(3): - line_text = pre_out_slice[lat_line + i + 1].strip().split() - for j in range(3): - lat_mat[i, j] = float(line_text[j]) - return lat_mat.T * bohr_to_ang - raise AttributeError("Lattice not found in pre_out_slice") - - def _get_initial_coords(self, pre_out_slice: list[str]) -> np.ndarray: - """Return initial coordinates. - - Return the initial coordinates from the pre_out_slice, corresponding to all data cut from JOutStructure list - initialization. This is needed to ensure coordinate data that is not being updated (and therefore not being - logged in the out file) is still available. - - Args: - pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that - contains the initial coordinates information. - - Returns: - np.ndarray: The initial coordinates. - """ - lines = self._get_ion_lines(pre_out_slice) - coords = np.zeros([len(lines), 3]) - for i, line in enumerate(lines): - line_text = pre_out_slice[line].strip().split()[2:] - for j in range(3): - coords[i, j] = float(line_text[j]) - coords_type_lines = find_first_range_key("coords-type", pre_out_slice) - if len(coords_type_lines): - coords_type = pre_out_slice[coords_type_lines[0]].strip().split()[1] - if coords_type.lower() != "cartesian": - coords = np.dot(coords, self._get_initial_lattice(pre_out_slice)) - return coords - - def _get_initial_species(self, pre_out_slice: list[str]) -> list[str]: - """Return initial species. - - Return the initial species from the pre_out_slice, corresponding to all data cut from JOutStructure list - initialization. This is needed to ensure species data that is not being updated (and therefore not being - logged in the out file) is still available. - - Args: - pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that - contains the initial species information. - - Returns: - list[str]: The initial species. - """ - lines = self._get_ion_lines(pre_out_slice) - species_strs = [] - for line in lines: - species_strs.append(pre_out_slice[line].strip().split()[1]) - return species_strs - - def _get_ion_lines(self, pre_out_slice: list[str]) -> list[int]: - """Return ion lines. - - Return the ion lines from the pre_out_slice, ensuring that all the ion lines are consecutive. - - Args: - pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that - contains the ion lines information. - - Returns: - list[int]: The ion lines. - """ - _lines = find_first_range_key("ion ", pre_out_slice) - if not len(_lines): - raise AttributeError("Ion lines not found in pre_out_slice") - gaps = [_lines[i + 1] - _lines[i] for i in range(len(_lines) - 1)] - if not all(g == 1 for g in gaps): - # TODO: Write the fix for this case - raise AttributeError("Ion lines not consecutive in pre_out_slice") - return _lines - - def _get_joutstructure_list( - self, out_slice: list[str], init_structure: Structure | None = None - ) -> list[JOutStructure]: - """Return list of JOutStructure objects. - - Get list of JStructure objects by splitting out_slice into slices and constructing - a JOutStructure object for each slice. Used in initialization. - - Args: - out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx). - init_structure (Structure | None): The initial structure if available, otherwise None. - - Returns: - list[JOutStructure]: The list of JOutStructure objects. - """ - out_bounds = _get_joutstructure_step_bounds(out_slice) - joutstructure_list: list[Structure | JOutStructure] = [] - for i, bounds in enumerate(out_bounds): - if i > 0: - init_structure = joutstructure_list[-1] - joutstructure_list.append( - JOutStructure._from_text_slice( - out_slice[bounds[0] : bounds[1]], - init_structure=init_structure, - opt_type=self.opt_type, - ) - ) - return joutstructure_list - - def _set_joutstructure_list(self, out_slice: list[str], init_structure: Structure | None = None) -> None: - """Set list of JOutStructure objects to slices. - - Set the list of JOutStructure objects to the slices attribute. - - Args: - out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx). - init_structure (Structure | None): The initial structure if available, otherwise None. - """ - out_list = self._get_joutstructure_list(out_slice, init_structure=init_structure) - for jos in out_list: - self.slices.append(jos) - def _check_convergence(self) -> None: """Set convergence flags. @@ -542,34 +190,6 @@ def _check_convergence(self) -> None: self.geom_converged = True self.geom_converged_reason = jst.geom_converged_reason - # This method is likely never going to be called as all (currently existing) - # attributes of the most recent slice are explicitly defined as a class - # property. However, it is included to reduce the likelihood of errors - # upon future changes to downstream code. - def __getattr__(self, name: str) -> Any: - """Return attribute value. - - Args: - name (str): The name of the attribute. - - Returns: - Any: The value of the attribute. - """ - if name in self.__dict__: - return self.__dict__[name] - - # Check if the attribute is a property of the class - for cls in inspect.getmro(self.__class__): - if name in cls.__dict__ and isinstance(cls.__dict__[name], property): - return cls.__dict__[name].__get__(self) - - # Check if the attribute is in self.jstrucs - if hasattr(self.slices[-1], name): - return getattr(self.slices[-1], name) - - # If the attribute is not found in either, raise an AttributeError - raise AttributeError(f"{self.__class__.__name__} not found: {name}") - def __getitem__(self, key: int | str) -> JOutStructure | Any: """Return item. @@ -682,3 +302,156 @@ def _get_joutstructures_start_idx( if out_slice_start_flag in line: return i return None + + +def _get_init_structure(pre_out_slice: list[str]) -> Structure | None: + """ + Return initial structure. + + Return the initial structure from the pre_out_slice, corresponding to all data cut from JOutStructure list + initialization. This is needed to ensure structural data that is not being updated (and therefore not being + logged in the out file) is still available. + + Args: + pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that + contains the initial structure information. + + Returns: + Structure | None: The initial structure if available, otherwise None. + """ + try: + lat_mat = _get_initial_lattice(pre_out_slice) + coords = _get_initial_coords(pre_out_slice) + species = _get_initial_species(pre_out_slice) + return Structure(lattice=lat_mat, species=species, coords=coords) + except AttributeError: + return None + + +def _get_initial_lattice(pre_out_slice: list[str]) -> np.ndarray: + """Return initial lattice. + + Return the initial lattice from the pre_out_slice, corresponding to all data cut from JOutStructure list + initialization. This is needed to ensure lattice data that is not being updated (and therefore not being + logged in the out file) is still available. + + Args: + pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that + contains the initial lattice information. + + Returns: + np.ndarray: The initial lattice matrix. + """ + lat_lines = find_first_range_key("lattice ", pre_out_slice) + if len(lat_lines): + lat_line = lat_lines[0] + lat_mat = np.zeros([3, 3]) + for i in range(3): + line_text = pre_out_slice[lat_line + i + 1].strip().split() + for j in range(3): + lat_mat[i, j] = float(line_text[j]) + return lat_mat.T * bohr_to_ang + raise AttributeError("Lattice not found in pre_out_slice") + + +def _get_initial_coords(pre_out_slice: list[str]) -> np.ndarray: + """Return initial coordinates. + + Return the initial coordinates from the pre_out_slice, corresponding to all data cut from JOutStructure list + initialization. This is needed to ensure coordinate data that is not being updated (and therefore not being + logged in the out file) is still available. + + Args: + pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that + contains the initial coordinates information. + + Returns: + np.ndarray: The initial coordinates. + """ + lines = _get_ion_lines(pre_out_slice) + coords = np.zeros([len(lines), 3]) + for i, line in enumerate(lines): + line_text = pre_out_slice[line].strip().split()[2:] + for j in range(3): + coords[i, j] = float(line_text[j]) + coords_type_lines = find_first_range_key("coords-type", pre_out_slice) + if len(coords_type_lines): + coords_type = pre_out_slice[coords_type_lines[0]].strip().split()[1] + if coords_type.lower() != "cartesian": + coords = np.dot(coords, _get_initial_lattice(pre_out_slice)) + return coords + + +def _get_initial_species(pre_out_slice: list[str]) -> list[str]: + """Return initial species. + + Return the initial species from the pre_out_slice, corresponding to all data cut from JOutStructure list + initialization. This is needed to ensure species data that is not being updated (and therefore not being + logged in the out file) is still available. + + Args: + pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that + contains the initial species information. + + Returns: + list[str]: The initial species. + """ + lines = _get_ion_lines(pre_out_slice) + species_strs = [] + for line in lines: + species_strs.append(pre_out_slice[line].strip().split()[1]) + return species_strs + + +def _get_ion_lines(pre_out_slice: list[str]) -> list[int]: + """Return ion lines. + + Return the ion lines from the pre_out_slice, ensuring that all the ion lines are consecutive. + + Args: + pre_out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx) that + contains the ion lines information. + + Returns: + list[int]: The ion lines. + """ + _lines = find_first_range_key("ion ", pre_out_slice) + if not len(_lines): + raise AttributeError("Ion lines not found in pre_out_slice") + gaps = [_lines[i + 1] - _lines[i] for i in range(len(_lines) - 1)] + if not all(g == 1 for g in gaps): + # TODO: Write the fix for this case + raise AttributeError("Ion lines not consecutive in pre_out_slice") + return _lines + + +def _get_joutstructure_list( + out_slice: list[str], + opt_type: str, + init_structure: Structure | None = None, +) -> list[JOutStructure]: + """Return list of JOutStructure objects. + + Get list of JStructure objects by splitting out_slice into slices and constructing + a JOutStructure object for each slice. Used in initialization. + + Args: + out_slice (list[str]): A slice of a JDFTx out file (individual call of JDFTx). + init_structure (Structure | None): The initial structure if available, otherwise None. + + Returns: + list[JOutStructure]: The list of JOutStructure objects. + """ + out_bounds = _get_joutstructure_step_bounds(out_slice) + joutstructure_list: list[Structure | JOutStructure] = [] + for i, bounds in enumerate(out_bounds): + if i > 0: + init_structure = joutstructure_list[-1] + joutstructure_list.append( + JOutStructure._from_text_slice( + out_slice[bounds[0] : bounds[1]], + init_structure=init_structure, + opt_type=opt_type, + ) + ) + return joutstructure_list diff --git a/src/pymatgen/io/jdftx/outputs.py b/src/pymatgen/io/jdftx/outputs.py index 41680e4053f..03046ba514e 100644 --- a/src/pymatgen/io/jdftx/outputs.py +++ b/src/pymatgen/io/jdftx/outputs.py @@ -35,6 +35,85 @@ class is written. __author__ = "Ben Rich, Jacob Clary" +_jof_atr_from_last_slice = [ + "prefix", + "jstrucs", + "jsettings_fluid", + "jsettings_electronic", + "jsettings_lattice", + "jsettings_ionic", + "xc_func", + "lattice_initial", + "lattice_final", + "lattice", + "a", + "b", + "c", + "fftgrid", + "geom_opt", + "geom_opt_type", + "efermi", + "egap", + "emin", + "emax", + "homo", + "lumo", + "homo_filling", + "lumo_filling", + "is_metal", + "converged", + "etype", + "broadening_type", + "broadening", + "kgrid", + "truncation_type", + "truncation_radius", + "pwcut", + "rhocut", + "pp_type", + "total_electrons", + "semicore_electrons", + "valence_electrons", + "total_electrons_uncharged", + "semicore_electrons_uncharged", + "valence_electrons_uncharged", + "nbands", + "atom_elements", + "atom_elements_int", + "atom_types", + "spintype", + "nspin", + "nat", + "atom_coords_initial", + "atom_coords_final", + "atom_coords", + "structure", + "has_solvation", + "fluid", + "is_gc", + "eopt_type", + "elecmindata", + "stress", + "strain", + "nstep", + "e", + "grad_k", + "alpha", + "linmin", + "abs_magneticmoment", + "tot_magneticmoment", + "mu", + "elec_nstep", + "elec_e", + "elec_grad_k", + "elec_alpha", + "elec_linmin", + "electronic_output", + "t_s", + "ecomponents", +] + + @dataclass class JDFTXOutfile: """ @@ -275,82 +354,9 @@ def from_file(cls, file_path: str | Path, is_bgw: bool = False, none_slice_on_er def __post_init__(self): if len(self.slices): - self.prefix = self.slices[-1].prefix - self.jstrucs = self.slices[-1].jstrucs - self.jsettings_fluid = self.slices[-1].jsettings_fluid - self.jsettings_electronic = self.slices[-1].jsettings_electronic - self.jsettings_lattice = self.slices[-1].jsettings_lattice - self.jsettings_ionic = self.slices[-1].jsettings_ionic - self.xc_func = self.slices[-1].xc_func - self.lattice_initial = self.slices[-1].lattice_initial - self.lattice_final = self.slices[-1].lattice_final - self.lattice = self.slices[-1].lattice - self.a = self.slices[-1].a - self.b = self.slices[-1].b - self.c = self.slices[-1].c - self.fftgrid = self.slices[-1].fftgrid - self.geom_opt = self.slices[-1].geom_opt - self.geom_opt_type = self.slices[-1].geom_opt_type - self.efermi = self.slices[-1].efermi - self.egap = self.slices[-1].egap - self.emin = self.slices[-1].emin - self.emax = self.slices[-1].emax - self.homo = self.slices[-1].homo - self.lumo = self.slices[-1].lumo - self.homo_filling = self.slices[-1].homo_filling - self.lumo_filling = self.slices[-1].lumo_filling - self.is_metal = self.slices[-1].is_metal - self.converged = self.slices[-1].converged - self.etype = self.slices[-1].etype - self.broadening_type = self.slices[-1].broadening_type - self.broadening = self.slices[-1].broadening - self.kgrid = self.slices[-1].kgrid - self.truncation_type = self.slices[-1].truncation_type - self.truncation_radius = self.slices[-1].truncation_radius - self.pwcut = self.slices[-1].pwcut - self.rhocut = self.slices[-1].rhocut - self.pp_type = self.slices[-1].pp_type - self.total_electrons = self.slices[-1].total_electrons - self.semicore_electrons = self.slices[-1].semicore_electrons - self.valence_electrons = self.slices[-1].valence_electrons - self.total_electrons_uncharged = self.slices[-1].total_electrons_uncharged - self.semicore_electrons_uncharged = self.slices[-1].semicore_electrons_uncharged - self.valence_electrons_uncharged = self.slices[-1].valence_electrons_uncharged - self.nbands = self.slices[-1].nbands - self.atom_elements = self.slices[-1].atom_elements - self.atom_elements_int = self.slices[-1].atom_elements_int - self.atom_types = self.slices[-1].atom_types - self.spintype = self.slices[-1].spintype - self.nspin = self.slices[-1].nspin - self.nat = self.slices[-1].nat - self.atom_coords_initial = self.slices[-1].atom_coords_initial - self.atom_coords_final = self.slices[-1].atom_coords_final - self.atom_coords = self.slices[-1].atom_coords - self.structure = self.slices[-1].structure + for var in _jof_atr_from_last_slice: + setattr(self, var, getattr(self.slices[-1], var)) self.trajectory = self._get_trajectory() - self.has_solvation = self.slices[-1].has_solvation - self.fluid = self.slices[-1].fluid - self.is_gc = self.slices[-1].is_gc - self.eopt_type = self.slices[-1].eopt_type - self.elecmindata = self.slices[-1].elecmindata - self.stress = self.slices[-1].stress - self.strain = self.slices[-1].strain - self.nstep = self.slices[-1].nstep - self.e = self.slices[-1].e - self.grad_k = self.slices[-1].grad_k - self.alpha = self.slices[-1].alpha - self.linmin = self.slices[-1].linmin - self.abs_magneticmoment = self.slices[-1].abs_magneticmoment - self.tot_magneticmoment = self.slices[-1].tot_magneticmoment - self.mu = self.slices[-1].mu - self.elec_nstep = self.slices[-1].elec_nstep - self.elec_e = self.slices[-1].elec_e - self.elec_grad_k = self.slices[-1].elec_grad_k - self.elec_alpha = self.slices[-1].elec_alpha - self.elec_linmin = self.slices[-1].elec_linmin - self.electronic_output = self.slices[-1].electronic_output - self.t_s = self.slices[-1].t_s - self.ecomponents = self.slices[-1].ecomponents def _get_trajectory(self) -> Trajectory: """Set the trajectory attribute of the JDFTXOutfile object.""" From 52f507fbf1fbf377d8ccd061d538fe42f3035829 Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 21:37:23 -0700 Subject: [PATCH 6/8] removing now unneeded __getattr__ from JDFTXOutfileSlice, no longer have properties in JDFTXOutfile --- src/pymatgen/io/jdftx/jdftxoutfileslice.py | 54 +--------------------- src/pymatgen/io/jdftx/outputs.py | 4 -- 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/src/pymatgen/io/jdftx/jdftxoutfileslice.py b/src/pymatgen/io/jdftx/jdftxoutfileslice.py index cba8dd50571..9656b43ceae 100644 --- a/src/pymatgen/io/jdftx/jdftxoutfileslice.py +++ b/src/pymatgen/io/jdftx/jdftxoutfileslice.py @@ -10,7 +10,7 @@ import math import pprint from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, ClassVar +from typing import TYPE_CHECKING, ClassVar import numpy as np @@ -420,27 +420,6 @@ def _from_out_slice_init_all_post_init(self) -> None: self._set_t_s() self._set_converged() self._set_electronic_output() - # - # if self.jstrucs is not None: - # self._set_trajectory() - # self.structure = self.jstrucs[-1] - # self.eopt_type = self.jstrucs.eopt_type - # self.elecmindata = self.jstrucs.elecmindata - # self.stress = self.jstrucs.stress - # self.strain = self.jstrucs.strain - # self.nstep = self.jstrucs.nstep - # self.e = self.jstrucs.e - # self.grad_k = self.jstrucs.grad_k - # self.alpha = self.jstrucs.alpha - # self.linmin = self.jstrucs.linmin - # self.abs_magneticmoment = self.jstrucs.abs_magneticmoment - # self.tot_magneticmoment = self.jstrucs.tot_magneticmoment - # self.mu = self._get_mu() - # self.elec_nstep = self.jstrucs.elec_nstep - # self.elec_e = self.jstrucs.elec_e - # self.elec_grad_k = self.jstrucs.elec_grad_k - # self.elec_alpha = self.jstrucs.elec_alpha - # self.elec_linmin = self.jstrucs.elec_linmin def _get_xc_func(self, text: list[str]) -> str | None: """Get the exchange-correlation functional used in the calculation. @@ -1151,37 +1130,6 @@ def to_dict(self) -> dict: dct[name] = getattr(self, name) return dct - # This method is likely never going to be called as all (currently existing) - # attributes of the most recent slice are explicitly defined as a class - # property. However, it is included to reduce the likelihood of errors - # upon future changes to downstream code. - def __getattr__(self, name: str) -> Any: - """Return attribute value. - - Args: - name (str): The name of the attribute. - - Returns: - Any: The value of the attribute. - - Raises: - AttributeError: If the attribute is not found. - """ - if name in self.__dict__: - return self.__dict__[name] - - # Check if the attribute is a property of the class - for cls in inspect.getmro(self.__class__): - if name in cls.__dict__ and isinstance(cls.__dict__[name], property): - return cls.__dict__[name].__get__(self) - - # Check if the attribute is in self.jstrucs - if hasattr(self.jstrucs, name): - return getattr(self.jstrucs, name) - - # If the attribute is not found in either, raise an AttributeError - raise AttributeError(f"{self.__class__.__name__} not found: {name}") - def __repr__(self) -> str: """Return string representation. diff --git a/src/pymatgen/io/jdftx/outputs.py b/src/pymatgen/io/jdftx/outputs.py index 03046ba514e..ac9f014f5d3 100644 --- a/src/pymatgen/io/jdftx/outputs.py +++ b/src/pymatgen/io/jdftx/outputs.py @@ -8,7 +8,6 @@ class is written. from __future__ import annotations -import inspect import pprint from dataclasses import dataclass, field from typing import TYPE_CHECKING, Any @@ -385,9 +384,6 @@ def to_dict(self) -> dict: if hasattr(self, fld): value = getattr(self, fld) dct[fld] = value - - for name, _obj in inspect.getmembers(type(self), lambda o: isinstance(o, property)): - dct[name] = getattr(self, name) return dct ########################################################################### From 9ac207cf39a2b6f67bd51544933d0f9c8e8e6eba Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 21:59:46 -0700 Subject: [PATCH 7/8] consistent "to_dict" methods --- src/pymatgen/io/jdftx/jdftxoutfileslice.py | 12 ++++---- src/pymatgen/io/jdftx/jelstep.py | 33 ++++++++++++++++++++++ src/pymatgen/io/jdftx/joutstructure.py | 16 +++++++++++ src/pymatgen/io/jdftx/joutstructures.py | 27 +++++++++++++++--- src/pymatgen/io/jdftx/outputs.py | 8 ++++-- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/pymatgen/io/jdftx/jdftxoutfileslice.py b/src/pymatgen/io/jdftx/jdftxoutfileslice.py index 9656b43ceae..8003ca3468f 100644 --- a/src/pymatgen/io/jdftx/jdftxoutfileslice.py +++ b/src/pymatgen/io/jdftx/jdftxoutfileslice.py @@ -1122,12 +1122,12 @@ def to_dict(self) -> dict: dict: JDFTXOutfileSlice in dictionary format. """ dct = {} - for field in self.__dataclass_fields__: - value = getattr(self, field) - dct[field] = value - - for name, _obj in inspect.getmembers(type(self), lambda o: isinstance(o, property)): - dct[name] = getattr(self, name) + for fld in self.__dataclass_fields__: + value = getattr(self, fld) + if hasattr(value, "to_dict"): + dct[fld] = value.to_dict() + else: + dct[fld] = value return dct def __repr__(self) -> str: diff --git a/src/pymatgen/io/jdftx/jelstep.py b/src/pymatgen/io/jdftx/jelstep.py index f56a50c7d98..942019d8738 100644 --- a/src/pymatgen/io/jdftx/jelstep.py +++ b/src/pymatgen/io/jdftx/jelstep.py @@ -216,6 +216,21 @@ def _set_nelectrons(self, fillings_line: str) -> None: """ self.nelectrons = get_colon_var_t1(fillings_line, "nElectrons: ") + def to_dict(self) -> dict: + """Return dictionary representation of JElStep object. + + Returns: + dict: Dictionary representation of JElStep object. + """ + dct = {} + for fld in self.__dataclass_fields__: + value = getattr(self, fld) + if hasattr(value, "to_dict"): + dct[fld] = value.to_dict() + else: + dct[fld] = value + return dct + def __str__(self) -> str: """ Return string representation of JElStep object. @@ -342,6 +357,24 @@ def __post_init__(self) -> None: for var in _jelsteps_atrs_from_last_slice: setattr(self, var, getattr(self.slices[-1], var)) + def to_dict(self) -> dict[str, Any]: + """Return dictionary representation of JElSteps object. + + Returns: + dict: Dictionary representation of JElSteps object. + """ + dct = {} + for fld in self.__dataclass_fields__: + if fld == "slices": + dct[fld] = [slc.to_dict() for slc in self.slices] + continue + value = getattr(self, fld) + if hasattr(value, "to_dict"): + dct[fld] = value.to_dict() + else: + dct[fld] = value + return dct + def __getitem__(self, key: int | str) -> JElStep | Any: """Return item. diff --git a/src/pymatgen/io/jdftx/joutstructure.py b/src/pymatgen/io/jdftx/joutstructure.py index 41477e46625..38fbe33e78a 100644 --- a/src/pymatgen/io/jdftx/joutstructure.py +++ b/src/pymatgen/io/jdftx/joutstructure.py @@ -680,6 +680,22 @@ def _collect_generic_line(self, line_text: str, generic_lines: list[str]) -> tup generic_lines.append(line_text) return generic_lines, collecting, collected + def to_dict(self) -> dict: + """ + Convert the JOutStructure object to a dictionary. + + Returns: + dict: A dictionary representation of the JOutStructure object. + """ + dct = {} + for fld in self.__dict__: + value = getattr(self, fld) + if hasattr(value, "to_dict"): + dct[fld] = value.to_dict() + else: + dct[fld] = value + return dct + # This method is likely never going to be called as all (currently existing) # attributes of the most recent slice are explicitly defined as a class # property. However, it is included to reduce the likelihood of errors diff --git a/src/pymatgen/io/jdftx/joutstructures.py b/src/pymatgen/io/jdftx/joutstructures.py index b060efec7dd..322d5d3f75c 100644 --- a/src/pymatgen/io/jdftx/joutstructures.py +++ b/src/pymatgen/io/jdftx/joutstructures.py @@ -190,6 +190,25 @@ def _check_convergence(self) -> None: self.geom_converged = True self.geom_converged_reason = jst.geom_converged_reason + def to_dict(self) -> dict: + """ + Convert the JOutStructures object to a dictionary. + + Returns: + dict: A dictionary representation of the JOutStructures object. + """ + dct = {} + for fld in self.__dataclass_fields__: + if fld == "slices": + dct[fld] = [slc.to_dict() for slc in self.slices] + continue + value = getattr(self, fld) + if hasattr(value, "to_dict"): + dct[fld] = value.to_dict() + else: + dct[fld] = value + return dct + def __getitem__(self, key: int | str) -> JOutStructure | Any: """Return item. @@ -201,12 +220,12 @@ def __getitem__(self, key: int | str) -> JOutStructure | Any: """ val = None if type(key) is int: - val = self.getitem_int(key) + val = self._getitem_int(key) if type(key) is str: - val = self.getitem_str(key) + val = self._getitem_str(key) return val - def getitem_int(self, key: int) -> JOutStructure: + def _getitem_int(self, key: int) -> JOutStructure: """Return a JOutStructure object. Args: @@ -217,7 +236,7 @@ def getitem_int(self, key: int) -> JOutStructure: """ return self.slices[key] - def getitem_str(self, key: str) -> Any: + def _getitem_str(self, key: str) -> Any: """Return attribute value. Args: diff --git a/src/pymatgen/io/jdftx/outputs.py b/src/pymatgen/io/jdftx/outputs.py index ac9f014f5d3..f61ea35360b 100644 --- a/src/pymatgen/io/jdftx/outputs.py +++ b/src/pymatgen/io/jdftx/outputs.py @@ -381,9 +381,11 @@ def to_dict(self) -> dict: """ dct = {} for fld in self.__dataclass_fields__: - if hasattr(self, fld): - value = getattr(self, fld) - dct[fld] = value + if fld == "slices": + dct[fld] = [slc.to_dict() for slc in self.slices] + continue + value = getattr(self, fld) + dct[fld] = value return dct ########################################################################### From b4eb0596bcc7cc4fd408f0ede5361b9ef30cc6df Mon Sep 17 00:00:00 2001 From: Ben Rich Date: Mon, 2 Dec 2024 22:02:08 -0700 Subject: [PATCH 8/8] Removing references to properties --- src/pymatgen/io/jdftx/jdftxoutfileslice.py | 1 + src/pymatgen/io/jdftx/joutstructure.py | 32 +--------------------- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/src/pymatgen/io/jdftx/jdftxoutfileslice.py b/src/pymatgen/io/jdftx/jdftxoutfileslice.py index 8003ca3468f..5976598ccc2 100644 --- a/src/pymatgen/io/jdftx/jdftxoutfileslice.py +++ b/src/pymatgen/io/jdftx/jdftxoutfileslice.py @@ -1130,6 +1130,7 @@ def to_dict(self) -> dict: dct[fld] = value return dct + # TODO: Re-do this now that there are no properties def __repr__(self) -> str: """Return string representation. diff --git a/src/pymatgen/io/jdftx/joutstructure.py b/src/pymatgen/io/jdftx/joutstructure.py index 38fbe33e78a..251859c3573 100644 --- a/src/pymatgen/io/jdftx/joutstructure.py +++ b/src/pymatgen/io/jdftx/joutstructure.py @@ -5,9 +5,8 @@ from __future__ import annotations -import inspect import pprint -from typing import Any, ClassVar +from typing import ClassVar import numpy as np @@ -696,35 +695,6 @@ def to_dict(self) -> dict: dct[fld] = value return dct - # This method is likely never going to be called as all (currently existing) - # attributes of the most recent slice are explicitly defined as a class - # property. However, it is included to reduce the likelihood of errors - # upon future changes to downstream code. - def __getattr__(self, name: str) -> Any: - """Return attribute value. - - Args: - name (str): The name of the attribute. - - Returns: - Any: The value of the attribute. - """ - # Only works for actual attributes of the class - if name in self.__dict__: - return self.__dict__[name] - - # Extended for properties - for cls in inspect.getmro(self.__class__): - if name in cls.__dict__ and isinstance(cls.__dict__[name], property): - return cls.__dict__[name].__get__(self) - - # Check if the attribute is in self.jstrucs - if hasattr(self.elecmindata, name): - return getattr(self.elecmindata, name) - - # If the attribute is not found in either, raise an AttributeError - raise AttributeError(f"{self.__class__.__name__} not found: {name}") - # TODO: Add string representation for JOutStructure-specific meta-data # This method currently only returns the Structure Summary as inherited from # the pymatgen Structure class.