From eaf96cea509da0e5aff0816a13741d4e7c512bd0 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 13 Mar 2024 12:12:51 -0400 Subject: [PATCH] Redefine `element_changes` for `Defect` for fast formation energy / concentration calculations --- doped/core.py | 36 +++++++++++++++++++++++++++++++++++- doped/generation.py | 9 ++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/doped/core.py b/doped/core.py index 0b7c8e56..31cd0312 100644 --- a/doped/core.py +++ b/doped/core.py @@ -818,7 +818,7 @@ def get_eigenvalue_analysis( return get_eigenvalue_analysis(self, plot=plot, filename=filename, **kwargs) - def _get_chempot_term(self, chemical_potentials=None): + def _get_chempot_term(self, chemical_potentials=None) -> float: chemical_potentials = chemical_potentials or {} element_changes = {elt.symbol: change for elt, change in self.defect.element_changes.items()} missing_elts = [elt for elt in element_changes if elt not in chemical_potentials] @@ -2044,6 +2044,21 @@ def get_charge_states(self, padding: int = 1) -> list[int]: return charges + def __setattr__(self, name, value): + """ + Handle attribute updates. + + Safety function to ensure properties (``defect_site``, ``volume``, + ``element_changes``) are recomputed whenever any defect attributes + are changed, to ensure consistency and correct predictions. + """ + super().__setattr__(name, value) + if name in ["site", "structure"]: + # delete internal pre-computed attributes, so they are re-computed when needed: + for attr in ["_defect_site", "_volume", "_element_changes"]: + if hasattr(self, attr): + delattr(self, attr) + @property def defect_site(self) -> PeriodicSite: """ @@ -2084,6 +2099,25 @@ def volume(self) -> float: return self._volume + @property + def element_changes(self) -> dict[Element, int]: + """ + The stoichiometry changes of the defect, as a dict. + + e.g. {"Mg": -1, "O": +1} for a O-on-Mg antisite in MgO. + Redefined from the ``pymatgen-analysis-defects`` method + to be far more efficient when used in loops (e.g. for + calculating defect concentrations as functions of chemical + potentials, temperature etc.). + + Returns: + dict[Element, int]: The species changes of the defect. + """ + if not hasattr(self, "_element_changes"): + self._element_changes = super().element_changes + + return self._element_changes + def __hash__(self): """ Hash the ``Defect`` object, based on the defect name and site. diff --git a/doped/generation.py b/doped/generation.py index f1fd02f0..43dd1ae7 100644 --- a/doped/generation.py +++ b/doped/generation.py @@ -2213,8 +2213,8 @@ def __str__(self): def __repr__(self): """ - Returns a string representation of the DefectsGenerator object, and - prints the DefectsGenerator info. + Returns a string representation of the ``DefectsGenerator`` object, and + prints the ``DefectsGenerator`` info. Note that Wyckoff letters can depend on the ordering of elements in the conventional standard structure, for which doped uses the ``spglib`` @@ -2300,7 +2300,10 @@ def _sort_defect_entries( for defect_entry in defect_entries_dict.values(): extrinsic_element_list.extend( el.symbol - for el in defect_entry.defect.defect_structure.composition.elements + for el in [ + *defect_entry.defect.structure.composition.elements, + *defect_entry.defect.site.species.elements, + ] if el.symbol not in host_element_list )