diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1be8c147..1fcd963a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -67,11 +67,11 @@ v.2.3.3 ---------- - General robustness updates: - Updated file parsing to avoid hidden files. - - Sanity check in `DefectsGenerator` if input symmetry is `P1`. - - Add `NKRED` to `INCAR` mismatch tests. + - Sanity check in ``DefectsGenerator`` if input symmetry is ``P1``. + - Add ``NKRED`` to ``INCAR`` mismatch tests. - Re-parse config & spin degeneracies in concentration/symmetry functions if data not already present - (if user is porting `DefectEntry`s from older `doped` versions or manually). - - Avoid unnecessary `DeprecationWarning`s + (if user is porting ``DefectEntry``s from older ``doped`` versions or manually). + - Avoid unnecessary ``DeprecationWarning``s - Updated docs and linting v.2.3.2 diff --git a/docs/Tips.rst b/docs/Tips.rst index 85fed35f..cb3b9ff8 100644 --- a/docs/Tips.rst +++ b/docs/Tips.rst @@ -500,10 +500,12 @@ In the typical defect calculation workflow with ``doped`` (exemplified in the tu ``DefectsParser(output_path=".")`` – written to ``output_path``. The JSON filename can be set with e.g. ``DefectsParser(json_filename="custom_name.json")``, but the default is ``{Host Chemical Formula}_defect_dict.json``. + - Additionally, a ``voronoi_nodes.json`` file is saved to the bulk supercell calculation directory if any interstitial defects are parsed. This contains information about the Voronoi tessellation nodes in the host structure, which are used for analysing interstitial positions but can be somewhat costly to calculate – so are automatically saved to file once initially computed to reduce parsing times. + - Additionally, if following the recommended structure-searching approach with ``ShakeNBreak`` as shown in the tutorials, ``distortion_metadata.json`` files will be written to the top directory (``output_path``, containing distortion information about all defects) and to each defect directory (containing just the diff --git a/docs/docs_requirements.txt b/docs/docs_requirements.txt index 35333f6c..982f7e4e 100644 --- a/docs/docs_requirements.txt +++ b/docs/docs_requirements.txt @@ -1,7 +1,8 @@ -sphinx -myst-nb +sphinx>=7 +myst-nb>=1.0 recommonmark -renku-sphinx-theme +renku-sphinx-theme>=0.4.0 # can update to >0.5.0 when https://github.com/SwissDataScienceCenter/renku-sphinx-theme/pull/26 merged +sphinx_rtd_theme>=1.3.0 # can update to >2.0 when https://github.com/SwissDataScienceCenter/renku-sphinx-theme/pull/26 merged sphinx_click sphinx_design ase @@ -22,4 +23,4 @@ shakenbreak>=3.3.1 packaging pandas>=1.1.0 pydefect>=0.8.1 -vise>=0.9.0 \ No newline at end of file +vise>=0.9.0 diff --git a/docs/index.rst b/docs/index.rst index 461ee384..885300a1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,14 +94,14 @@ Studies using ``doped``, so far - Z\. Yuan & G. Hautier **First-principles study of defects and doping limits in CaO** `Applied Physics Letters `_ 2024 - B\. E. Murdock et al. **Li-Site Defects Induce Formation of Li-Rich Impurity Phases: Implications for Charge Distribution and Performance of LiNi** :sub:`0.5-x` **M** :sub:`x` **Mn** :sub:`1.5` **O** :sub:`4` **Cathodes (M = Fe and Mg; x = 0.05–0.2)** `Advanced Materials `_ 2024 -- A\. G. Squires et al. **Oxygen dimerization as a defect-driven process in bulk LiNiO₂** `ChemRxiv `_ 2024 +- A\. G. Squires et al. **Oxygen dimerization as a defect-driven process in bulk LiNiO₂** `ChemRxiv `__ 2024 - Y\. Fu & H. Lohan et al. **Factors Enabling Delocalized Charge-Carriers in Pnictogen-Based Solar Absorbers: In-depth Investigation into CuSbSe2** `arXiv `_ 2024 -- S\. Hachmioune et al. **Exploring the Thermoelectric Potential of MgB4: Electronic Band Structure, Transport Properties, and Defect Chemistry** `Chemistry of Materials `_ 2024 -- J\. Hu et al. **Enabling ionic transport in Li3AlP2 the roles of defects and disorder** `ChemRxiv `_ 2024 +- S\. Hachmioune et al. **Exploring the Thermoelectric Potential of MgB4: Electronic Band Structure, Transport Properties, and Defect Chemistry** `Chemistry of Materials `__ 2024 +- J\. Hu et al. **Enabling ionic transport in Li3AlP2 the roles of defects and disorder** `ChemRxiv `__ 2024 - X\. Wang et al. **Upper efficiency limit of Sb₂Se₃ solar cells** `Joule `_ 2024 -- I\. Mosquera-Lois et al. **Machine-learning structural reconstructions for accelerated point defect calculations** `npj Computational Materials `_ 2024 +- I\. Mosquera-Lois et al. **Machine-learning structural reconstructions for accelerated point defect calculations** `npj Computational Materials `__ 2024 - W\. Dou et al. **Band Degeneracy and Anisotropy Enhances Thermoelectric Performance from Sb₂Si₂Te₆ to Sc₂Si₂Te₆** `Journal of the American Chemical Society `_ 2024 -- K\. Li et al. **Computational Prediction of an Antimony-based n-type Transparent Conducting Oxide: F-doped Sb₂O₅** `Chemistry of Materials `_ 2024 +- K\. Li et al. **Computational Prediction of an Antimony-based n-type Transparent Conducting Oxide: F-doped Sb₂O₅** `Chemistry of Materials `__ 2024 - X\. Wang et al. **Four-electron negative-U vacancy defects in antimony selenide** `Physical Review B `_ 2023 - Y\. Kumagai et al. **Alkali Mono-Pnictides: A New Class of Photovoltaic Materials by Element Mutation** `PRX Energy `__ 2023 - S\. M. Liga & S. R. Kavanagh, A. Walsh, D. O. Scanlon, G. Konstantatos **Mixed-Cation Vacancy-Ordered Perovskites (Cs₂Ti** :sub:`1–x` **Sn** :sub:`x` **X₆; X = I or Br): Low-Temperature Miscibility, Additivity, and Tunable Stability** `Journal of Physical Chemistry C`_ 2023 @@ -111,7 +111,7 @@ Studies using ``doped``, so far - J\. Willis, K. B. Spooner, D. O. Scanlon. **On the possibility of p-type doping in barium stannate** `Applied Physics Letters `__ 2023 - J\. Cen et al. **Cation disorder dominates the defect chemistry of high-voltage LiMn** :sub:`1.5` **Ni** :sub:`0.5` **O₄ (LMNO) spinel cathodes** `Journal of Materials Chemistry A`_ 2023 - J\. Willis & R. Claes et al. **Limits to Hole Mobility and Doping in Copper Iodide** `Chemistry of Materials `__ 2023 -- I\. Mosquera-Lois & S. R. Kavanagh, A. Walsh, D. O. Scanlon **Identifying the ground state structures of point defects in solids** `npj Computational Materials`_ 2023 +- I\. Mosquera-Lois & S. R. Kavanagh, A. Walsh, D. O. Scanlon **Identifying the ground state structures of point defects in solids** `npj Computational Materials `__ 2023 - Y\. T. Huang & S. R. Kavanagh et al. **Strong absorption and ultrafast localisation in NaBiS₂ nanocrystals with slow charge-carrier recombination** `Nature Communications`_ 2022 - S\. R. Kavanagh, D. O. Scanlon, A. Walsh, C. Freysoldt **Impact of metastable defect structures on carrier recombination in solar cells** `Faraday Discussions`_ 2022 - Y-S\. Choi et al. **Intrinsic Defects and Their Role in the Phase Transition of Na-Ion Anode Na₂Ti₃O₇** `ACS Applied Energy Materials `__ 2022 @@ -129,7 +129,6 @@ Studies using ``doped``, so far .. _Journal of Physical Chemistry C: https://doi.org/10.1021/acs.jpcc.3c05204 .. _Journal of Materials Chemistry A: https://doi.org/10.1039/D3TA00532A -.. _npj Computational Materials: https://www.nature.com/articles/s41524-023-00973-1 .. _Nature Communications: https://www.nature.com/articles/s41467-022-32669-3 .. _Faraday Discussions: https://doi.org/10.1039/D2FD00043A .. _Chemical Science: https://doi.org/10.1039/D1SC03775G diff --git a/doped/thermodynamics.py b/doped/thermodynamics.py index 6025f060..5700ae84 100644 --- a/doped/thermodynamics.py +++ b/doped/thermodynamics.py @@ -191,6 +191,41 @@ def _parse_chempots(chempots: Optional[dict] = None, el_refs: Optional[dict] = N return chempots, chempots.get("elemental_refs") +def raw_energy_from_chempots(composition: Union[str, dict, Composition], chempots: dict) -> float: + """ + Given an input composition (as a ``str``, ``dict`` or ``pymatgen`` + ``Composition`` object) and chemical potentials dictionary, get the + corresponding raw energy of the composition (i.e. taking the energies given + in the ``'limits'`` subdicts of ``chempots``, in the ``doped`` chemical + potentials dictionary format). + + Args: + composition (Union[str, dict, Composition]): + Composition to get the raw energy of. + chempots (dict): + Chemical potentials dictionary. + + Returns: + Raw energy of the composition. + """ + if not isinstance(composition, Composition): + composition = Composition(composition) + + if "limits" not in chempots: + chempots, _el_refs = _parse_chempots(chempots) + + raw_energies_dict = dict(next(iter(chempots["limits"].values()))) + + if any(el.symbol not in raw_energies_dict for el in composition.elements): + raise ValueError( + f"The chemical potentials dictionary (with elements {list(raw_energies_dict.keys())} does not " + f"contain all the elements in the host composition " + f"({[el.symbol for el in composition.elements]})!" + ) + + return sum(raw_energies_dict.get(el.symbol, 0) * stoich for el, stoich in composition.items()) + + def group_defects_by_distance( entry_list: list[DefectEntry], dist_tol: float = 1.5 ) -> dict[str, dict[tuple, list[DefectEntry]]]: @@ -540,7 +575,7 @@ def _raise_VBM_band_gap_value_error(vals, type="VBM"): ) # order entries for deterministic behaviour (particularly for plotting) - self._sort_parse_and_check_entries(check_compatibility=check_compatibility) + self._sort_parse_and_check_entries() bulk_entry = self.defect_entries[0].bulk_entry if bulk_entry is not None: @@ -550,10 +585,11 @@ def _raise_VBM_band_gap_value_error(vals, type="VBM"): else: self.bulk_formula = None - def _sort_parse_and_check_entries(self, check_compatibility: bool = True): + def _sort_parse_and_check_entries(self): """ Sort the defect entries, parse the transition levels, and check the - compatibility of the bulk entries (if check_compatibility is True). + compatibility of the bulk entries (if ``self.check_compatibility`` is + ``True``). """ defect_entries_dict: dict[str, DefectEntry] = {} for entry in self.defect_entries: # rename defect entry names in dict if necessary ("_a", "_b"...) @@ -572,8 +608,9 @@ def _sort_parse_and_check_entries(self, check_compatibility: bool = True): with warnings.catch_warnings(): # ignore formation energies chempots warning when just parsing TLs warnings.filterwarnings("ignore", message="No chemical potentials") self._parse_transition_levels() - if check_compatibility: + if self.check_compatibility: self._check_bulk_compatibility() + self._check_bulk_chempots_compatibility(self._chempots) def as_dict(self): """ @@ -661,7 +698,11 @@ def _get_chempots(self, chempots: Optional[dict] = None, el_refs: Optional[dict] Parse chemical potentials, either using input values (after formatting them in the doped format) or using the class attributes if set. """ - return _parse_chempots(chempots or self.chempots, el_refs or self.el_refs) + chempots, el_refs = _parse_chempots(chempots or self.chempots, el_refs or self.el_refs) + if self.check_compatibility: + self._check_bulk_chempots_compatibility(chempots) + + return chempots, el_refs def _parse_transition_levels(self): r""" @@ -890,7 +931,7 @@ def _check_bulk_compatibility(self): """ Helper function to quickly check if all entries have compatible bulk calculation settings, by checking that the energy of - defect_entry.bulk_entry is the same for all defect entries. + ``defect_entry.bulk_entry`` is the same for all defect entries. By proxy checks that same bulk/defect calculation settings were used in all cases, from each bulk/defect combination already being checked when @@ -907,8 +948,8 @@ def _check_bulk_compatibility(self): f"eV. This can lead to inaccuracies in predicted formation energies! The bulk energies of " f"defect entries in `defect_entries` are:\n" f"{[(entry.name, entry.bulk_entry.energy) for entry in self.defect_entries]}\n" - f"You can suppress this warning by setting `check_compatibility=False` in " - "`DefectThermodynamics` initialisation." + f"You can suppress this warning by setting `DefectThermodynamics.check_compatibility = " + f"False`." ) def _check_bulk_defects_compatibility(self): @@ -918,7 +959,7 @@ def _check_bulk_defects_compatibility(self): Currently not used, as the bulk/defect compatibility is checked when parsing, and the compatibility across bulk calculations is checked with - _check_bulk_compatibility(). + ``_check_bulk_compatibility()``. """ # check each defect entry against its own bulk, and also check each bulk against each other reference_defect_entry = self.defect_entries[0] @@ -964,6 +1005,55 @@ def _check_bulk_defects_compatibility(self): f"{defect_entry.name}: \n{concatenated_warnings}" ) + def _check_bulk_chempots_compatibility(self, chempots: Optional[dict] = None): + r""" + Helper function to quickly check if the supplied chemical potentials + dictionary matches the bulk supercell used for the defect calculations, + by comparing the raw energies (from the bulk supercell calculation, and + that corresponding to the chemical potentials supplied). + + Args: + chempots (dict, optional): + Dictionary of chemical potentials to check compatibility with + the bulk supercell calculations (``DefectEntry.bulk_entry``\s), + in the ``doped`` format. + + If ``None`` (default), will use ``self.chempots`` (= 0 for all + chemical potentials by default). + This can have the form of ``{"limits": [{'limit': [chempot_dict]}]}`` + (the format generated by ``doped``\'s chemical potential parsing + functions (see tutorials)), or alternatively a dictionary of chemical + potentials for a single limit (``limit``), in the format: + ``{element symbol: chemical potential}``. + If manually specifying chemical potentials this way, you can set the + ``el_refs`` option with the DFT reference energies of the elemental phases, + in which case it is the formal chemical potentials (i.e. relative to the + elemental references) that should be given here, otherwise the absolute + (DFT) chemical potentials should be given. + """ + if chempots is None and self.chempots is None: + return + + bulk_entry = next(entry.bulk_entry for entry in self.defect_entries) + bulk_supercell_energy_per_atom = bulk_entry.energy / bulk_entry.composition.num_atoms + bulk_chempot_energy_per_atom = ( + raw_energy_from_chempots(bulk_entry.composition, chempots or self.chempots) + / bulk_entry.composition.num_atoms + ) + + if abs(bulk_supercell_energy_per_atom - bulk_chempot_energy_per_atom) > 0.025: + warnings.warn( # 0.05 eV intrinsic defect formation energy error tolerance, taking per-atom + # chempot error and multiplying by 2 to account for how this would affect antisite + # formation energies (extreme case) + f"Note that the raw (DFT) energy of the bulk supercell calculation (" + f"{bulk_supercell_energy_per_atom:.2f} eV/atom) differs from that expected from the " + f"supplied chemical potentials ({bulk_chempot_energy_per_atom:.2f} eV/atom) by >0.025 eV. " + f"This will likely give inaccuracies of similar magnitude in the predicted formation " + f"energies! \n" + f"You can suppress this warning by setting `DefectThermodynamics.check_compatibility = " + f"False`." + ) + def add_entries( self, defect_entries: Union[list[DefectEntry], dict[str, DefectEntry]], @@ -986,6 +1076,7 @@ def add_entries( entry (i.e. that all reference bulk energies are the same). (Default: True) """ + self.check_compatibility = check_compatibility if isinstance(defect_entries, dict): defect_entries = list(defect_entries.values()) @@ -996,7 +1087,7 @@ def add_entries( ) self._defect_entries += defect_entries - self._sort_parse_and_check_entries(check_compatibility=check_compatibility) + self._sort_parse_and_check_entries() @property def defect_entries(self): @@ -1061,6 +1152,8 @@ def chempots(self, input_chempots): (Default: None) """ self._chempots, self._el_refs = _parse_chempots(input_chempots, self._el_refs) + if self.check_compatibility: + self._check_bulk_chempots_compatibility(self._chempots) @property def el_refs(self): diff --git a/doped/utils/plotting.py b/doped/utils/plotting.py index 697dafe4..25c7924b 100644 --- a/doped/utils/plotting.py +++ b/doped/utils/plotting.py @@ -157,7 +157,7 @@ def format_defect_name( Format defect name for plot titles. (i.e. from Cd_i_C3v_0 to $Cd_{i}^{0}$ or $Cd_{i_{C3v}}^{0}$). - Note this assumes "V_" means vacancy not Vanadium. + Note this assumes "V_..." means vacancy not Vanadium. Args: defect_species (:obj:`str`): diff --git a/doped/utils/supercells.py b/doped/utils/supercells.py index e36c5f64..fc6ec42c 100644 --- a/doped/utils/supercells.py +++ b/doped/utils/supercells.py @@ -181,7 +181,7 @@ def _get_largest_cube_from_matrix(matrix: np.ndarray, max_ijk: int = 10): return np.min(np.linalg.norm(length_vecs, axis=1)) -def cell_metric(cell_matrix: np.ndarray, target: str = "SC") -> float: +def cell_metric(cell_matrix: np.ndarray, target: str = "SC", rms=True) -> float: """ Calculates the deviation of the given cell matrix from an ideal simple cubic (if target = "SC") or face-centred cubic (if target = "FCC") matrix, @@ -212,32 +212,33 @@ def cell_metric(cell_matrix: np.ndarray, target: str = "SC") -> float: deviation score from. Either "SC" for simple cubic or "FCC" for face-centred cubic. Default = "SC" + rms (bool): + Whether to return the `root` mean square (RMS) difference of + the vector lengths from that of the idealised values (default), + or just the mean square difference (to reduce computation time + when scanning over many possible matrices). + Returns: float: Cell metric (0 is perfect score) """ - eff_cubic_length = float(abs(np.linalg.det(cell_matrix)) ** (1 / 3)) - norms = np.linalg.norm(cell_matrix, axis=0) - - if target.upper() == "SC": - return round( - np.sqrt( # get rms difference to eff cubic - np.sum(((norms - eff_cubic_length) / eff_cubic_length) ** 2) - ), - 4, - ) # round to 4 decimal places to avoid tiny numerical differences messing with sorting - - if target.upper() != "FCC": + eff_cubic_length = np.abs(np.linalg.det(cell_matrix)) ** (1 / 3) + norms = np.linalg.norm(cell_matrix, axis=1) + + if target.upper() == "SC": # get rms/msd difference to eff cubic + deviations = (norms - eff_cubic_length) / eff_cubic_length + + elif target.upper() == "FCC": + # FCC is characterised by 60 degree angles & lattice vectors = 2**(1/6) times the eff cubic length + eff_fcc_length = eff_cubic_length * 2 ** (1 / 6) + deviations = (norms - eff_fcc_length) / eff_fcc_length + + else: raise ValueError(f"Allowed values for `target` are 'SC' or 'FCC'. Got {target}") - # FCC is characterised by 60 degree angles & lattice vectors = 2**(1/6) times the eff cubic length - eff_fcc_length = eff_cubic_length * 2 ** (1 / 6) - return round( - np.sqrt( # get rms difference to eff cubic - np.sum(((norms - eff_fcc_length) / eff_fcc_length) ** 2) - ), - 4, - ) # round to 4 decimal places to avoid tiny numerical differences messing with sorting + msd = np.sum(deviations**2) + # round to 4 decimal places to avoid tiny numerical differences messing with sorting: + return round(np.sqrt(msd), 4) if rms else round(msd, 4) def _lengths_and_angles_from_matrix(matrix: np.ndarray) -> tuple[Any, ...]: @@ -297,7 +298,9 @@ def _P_matrix_sorting_func(P: np.ndarray, cell: np.ndarray = None) -> tuple: Returns: tuple: Tuple of sorting criteria values """ - cubic_metric = cell_metric(np.matmul(P, cell)) if cell is not None else cell_metric(P) + cubic_metric = ( + cell_metric(np.matmul(P, cell), rms=False) if cell is not None else cell_metric(P, rms=False) + ) symmetric = np.allclose(P, P.T) abs_sum_off_diag = np.sum(np.abs(P) - np.abs(np.diag(np.diag(P)))) @@ -783,12 +786,14 @@ def find_optimal_cell_shape( label=target_shape, ) - score_list = [cell_metric(cell_matrix, target=target_shape) for cell_matrix in unique_cell_matrices] - best_score = np.min(score_list) + score_list = [ + cell_metric(cell_matrix, target=target_shape, rms=False) for cell_matrix in unique_cell_matrices + ] + best_msd = np.min(score_list) if verbose: - print(f"Best score: {best_score}") + print(f"Best score: {np.sqrt(best_msd)}") - best_score_indices = np.where(np.array(score_list) == best_score)[0] + best_score_indices = np.where(np.array(score_list) == best_msd)[0] optimal_P = _get_optimal_P( valid_P=valid_P, @@ -801,7 +806,4 @@ def find_optimal_cell_shape( cell=cell, ) - if verbose: - print(f"Score: {best_score}") - - return (optimal_P, best_score) if return_score else optimal_P + return (optimal_P, np.sqrt(best_msd)) if return_score else optimal_P diff --git a/tests/test_thermodynamics.py b/tests/test_thermodynamics.py index f578a1fd..427cfdd4 100644 --- a/tests/test_thermodynamics.py +++ b/tests/test_thermodynamics.py @@ -18,6 +18,7 @@ import pandas as pd import pytest from monty.serialization import dumpfn, loadfn +from pymatgen.core.composition import Composition from doped.generation import _sort_defect_entries from doped.thermodynamics import DefectThermodynamics, get_fermi_dos, scissor_dos @@ -102,6 +103,14 @@ def setUp(self): self.Sb2O5_defect_thermo = deepcopy(self.orig_Sb2O5_defect_thermo) self.ZnS_defect_thermo = deepcopy(self.orig_ZnS_defect_thermo) + self.cdte_chempot_warning_message = ( + "Note that the raw (DFT) energy of the bulk supercell calculation (-3.37 eV/atom) differs " + "from that expected from the supplied chemical potentials (-3.50 eV/atom) by >0.025 eV. This " + "will likely give inaccuracies of similar magnitude in the predicted formation energies! " + "\nYou can suppress this warning by setting `DefectThermodynamics.check_compatibility = " + "False`." + ) + @classmethod def setUpClass(cls): cls.module_path = os.path.dirname(os.path.abspath(__file__)) @@ -376,6 +385,17 @@ def _check_defect_thermo( self._set_and_check_dist_tol(1.0, defect_thermo, 4) self._set_and_check_dist_tol(0.5, defect_thermo, 5) + # test mismatching chempot warnings: + print("Checking mismatching chempots") + mismatch_chempots = {el: -3 for el in Composition(defect_thermo.bulk_formula).as_dict()} + with warnings.catch_warnings(record=True) as w: + defect_thermo.chempots = mismatch_chempots + print([str(warning.message) for warning in w]) # for debugging + assert any( + "Note that the raw (DFT) energy of the bulk supercell calculation" in str(warning.message) + for warning in w + ) + def _check_CdTe_example_dist_tol(self, defect_thermo, num_grouped_defects): print(f"Testing CdTe updated dist_tol: {defect_thermo.dist_tol}") tl_df = defect_thermo.get_transition_levels() @@ -436,6 +456,13 @@ def test_initialisation_and_attributes(self): el_refs=self.V2O5_chempots["elemental_refs"], ) + print(f"Checking {name} with mismatching chempots") + with warnings.catch_warnings(record=True) as w: + _defect_thermo = DefectThermodynamics(self.CdTe_defect_dict, chempots={"Cd": -1.0, "Te": -6}) + print([str(warning.message) for warning in w]) # for debugging + assert len(w) == 1 # only chempot incompatibility warning + assert str(w[0].message) == self.cdte_chempot_warning_message + def test_DefectsParser_thermo_objs(self): """ Test the `DefectThermodynamics` objects created from the @@ -1369,7 +1396,7 @@ def _check_formation_energy_methods(form_en_df_row, thermo_obj, fermi_level): el_refs={"Cd": -12, "Te": -1}, ) assert "Fermi level was not set" not in output - assert not w + assert len(w) == 1 # only mis-matching chempot warning assert manual_form_en_df.shape == (7, 10) # test sum of formation energy terms equals total and other formation energy df properties: self._check_form_en_df(manual_form_en_df, fermi_level=3, defect_thermo=self.CdTe_defect_thermo) @@ -1939,6 +1966,31 @@ def test_symmetry_degeneracy_unparsed(self): (e.g. transferring from old doped versions, from pymatgen-analysis- defects objects etc). """ + # TODO + + def test_incompatible_chempots_warning(self): + """ + Test that we get the expected warnings when we provide incompatible + chemical potentials for our DefectThermodynamics object. + """ + slightly_off_chempots = {"Cd": -1.0, "Te": -6} + for func in [ + self.CdTe_defect_thermo.get_equilibrium_concentrations, + self.CdTe_defect_thermo.get_dopability_limits, + self.CdTe_defect_thermo.get_doping_windows, + self.CdTe_defect_thermo.get_formation_energies, + self.CdTe_defect_thermo.plot, + ]: + _result, _tl_output, w = _run_func_and_capture_stdout_warnings( + func, chempots=slightly_off_chempots + ) + assert any(str(warning.message) == self.cdte_chempot_warning_message for warning in w) + + with warnings.catch_warnings(record=True) as w: + self.CdTe_defect_thermo.chempots = slightly_off_chempots + print([str(warning.message) for warning in w]) # for debugging + assert len(w) == 1 # only chempot incompatibility warning + assert str(w[0].message) == self.cdte_chempot_warning_message def belas_linear_fit(T): #