From 618ae1941b1f2299784584000ee56c20f645b3fe Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Tue, 30 Jul 2024 14:37:02 -0400 Subject: [PATCH 01/17] Test symmetries and degeneracies functions when re-parsing necessary --- tests/test_thermodynamics.py | 131 ++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 56 deletions(-) diff --git a/tests/test_thermodynamics.py b/tests/test_thermodynamics.py index 427cfdd4..9ca62d2d 100644 --- a/tests/test_thermodynamics.py +++ b/tests/test_thermodynamics.py @@ -411,6 +411,14 @@ def _set_and_check_dist_tol(self, dist_tol, defect_thermo, num_grouped_defects=0 defect_thermo.print_transition_levels() defect_thermo.print_transition_levels(all=True) + def _clear_symmetry_degeneracy_info(self, defect_thermo): + for defect_entry in defect_thermo.defect_entries: + for key in list(defect_entry.calculation_metadata.keys()): + if "symmetry" in key: + del defect_entry.calculation_metadata[key] + for key in list(defect_entry.degeneracy_factors.keys()): + del defect_entry.degeneracy_factors[key] + def test_initialisation_and_attributes(self): """ Test initialisation and attributes of DefectsThermodynamics. @@ -764,8 +772,37 @@ def test_get_transition_levels_CdTe(self): assert list(tl_df.iloc[4]) == ["Int_Te_3_a", "None", np.inf, False, 0] assert list(tl_df.iloc[5]) == ["Int_Te_3_b", "None", np.inf, False, 0] - def test_get_symmetries_degeneracies_CdTe(self): - sym_degen_df = self.CdTe_defect_thermo.get_symmetries_and_degeneracies() + def test_get_symmetries_degeneracies(self): + """ + Test symmetry and degeneracy functions. + + Also tests that the symmetry and degeneracy functions still work if the + symmetry and degeneracy metadata wasn't parsed earlier for any reason + (e.g. transferring from old doped versions, from pymatgen-analysis- + defects objects etc), with ``reset_thermo=True``. + """ + for reset_thermo in [False, True]: + for check_func, thermo in [ + (self._check_CdTe_symmetries_degeneracies, self.CdTe_defect_thermo), + ( + self._check_MgO_symmetries_degeneracies, + loadfn(f"{module_path}/../examples/MgO/MgO_thermo.json.gz"), + ), + (self._check_YTOS_symmetries_degeneracies, self.YTOS_defect_thermo), + (self._check_Sb2O5_symmetries_degeneracies, self.Sb2O5_defect_thermo), + ]: + print( + f"Checking symmetries & degeneracies for {thermo.bulk_formula}, reset_thermo" + f"={reset_thermo}" + ) + if reset_thermo: + self._clear_symmetry_degeneracy_info(thermo) + check_func(thermo) + + self.setUp() # reset thermo objects on 2nd run (with ``reset_thermo=True``) + + def _check_CdTe_symmetries_degeneracies(self, CdTe_defect_thermo: DefectThermodynamics): + sym_degen_df = CdTe_defect_thermo.get_symmetries_and_degeneracies() print(sym_degen_df) assert sym_degen_df.shape == (7, 8) assert list(sym_degen_df.columns) == [ @@ -799,12 +836,11 @@ def test_get_symmetries_degeneracies_CdTe(self): ) print(non_formatted_sym_degen_df) # for debugging for i, row in enumerate(cdte_sym_degen_lists): - row[1] = int(row[1]) + row[1] = int(row[1]) # type: ignore assert list(non_formatted_sym_degen_df.iloc[i]) == row - def test_get_symmetries_degeneracies_MgO(self): - MgO_thermo = loadfn(f"{module_path}/../examples/MgO/MgO_thermo.json.gz") - sym_degen_df = MgO_thermo.get_symmetries_and_degeneracies() + def _check_MgO_symmetries_degeneracies(self, MgO_defect_thermo: DefectThermodynamics): + sym_degen_df = MgO_defect_thermo.get_symmetries_and_degeneracies() # print(sym_degen_df) assert sym_degen_df.shape == (5, 8) assert list(sym_degen_df.columns) == [ @@ -831,47 +867,49 @@ def test_get_symmetries_degeneracies_MgO(self): print(i, row) assert list(sym_degen_df.iloc[i]) == row - non_formatted_sym_degen_df = MgO_thermo.get_symmetries_and_degeneracies(skip_formatting=True) - # print(non_formatted_sym_degen_df) # for debugging + non_formatted_sym_degen_df = MgO_defect_thermo.get_symmetries_and_degeneracies( + skip_formatting=True + ) + print(non_formatted_sym_degen_df) # for debugging for i, row in enumerate(mgo_sym_degen_lists): - row[1] = int(row[1]) + row[1] = int(row[1]) # type: ignore assert list(non_formatted_sym_degen_df.iloc[i]) == row - def test_get_symmetries_degeneracies_YTOS(self): - sym_degen_df = self.YTOS_defect_thermo.get_symmetries_and_degeneracies() + def _check_YTOS_symmetries_degeneracies(self, YTOS_defect_thermo: DefectThermodynamics): + sym_degen_df = YTOS_defect_thermo.get_symmetries_and_degeneracies() # hardcoded tests to ensure symmetry determination working as expected: assert any( list(sym_degen_df.iloc[i]) == ["Int_F", "-1", "Cs", "C4v", 0.25, 1, 0.25, 4.0] for i in range(sym_degen_df.shape[0]) ) - sym_degen_df = self.YTOS_defect_thermo.get_symmetries_and_degeneracies(symprec=0.1) + sym_degen_df = YTOS_defect_thermo.get_symmetries_and_degeneracies(symprec=0.1) assert any( list(sym_degen_df.iloc[i]) == ["Int_F", "-1", "Cs", "C4v", 0.25, 1, 0.25, 4.0] for i in range(sym_degen_df.shape[0]) ) - sym_degen_df = self.YTOS_defect_thermo.get_symmetries_and_degeneracies(symprec=0.01) + sym_degen_df = YTOS_defect_thermo.get_symmetries_and_degeneracies(symprec=0.01) assert any( list(sym_degen_df.iloc[i]) == ["Int_F", "-1", "Cs", "Cs", 1.0, 1, 1.0, 4.0] for i in range(sym_degen_df.shape[0]) ) - def test_get_symmetries_degeneracies_Sb2O5(self): - sym_degen_df = self.Sb2O5_defect_thermo.get_symmetries_and_degeneracies() + def _check_Sb2O5_symmetries_degeneracies(self, Sb2O5_defect_thermo: DefectThermodynamics): + sym_degen_df = Sb2O5_defect_thermo.get_symmetries_and_degeneracies() # hardcoded tests to ensure symmetry determination working as expected: assert any( list(sym_degen_df.iloc[i]) == ["inter_2_Sb", "0", "C1", "C1", 1.0, 2, 2.0, 4.0] for i in range(sym_degen_df.shape[0]) ) - sym_degen_df = self.Sb2O5_defect_thermo.get_symmetries_and_degeneracies(symprec=0.1) + sym_degen_df = Sb2O5_defect_thermo.get_symmetries_and_degeneracies(symprec=0.1) assert any( list(sym_degen_df.iloc[i]) == ["inter_2_Sb", "0", "C1", "C1", 1.0, 2, 2.0, 4.0] for i in range(sym_degen_df.shape[0]) ) - sym_degen_df = self.Sb2O5_defect_thermo.get_symmetries_and_degeneracies(symprec=0.2) + sym_degen_df = Sb2O5_defect_thermo.get_symmetries_and_degeneracies(symprec=0.2) assert any( list(sym_degen_df.iloc[i]) == ["inter_2_Sb", "0", "C1", "Ci", 0.5, 2, 1.0, 4.0] for i in range(sym_degen_df.shape[0]) @@ -1737,34 +1775,29 @@ def test_CdTe_all_intrinsic_defects(self): ) # test adjusting symprec: - if defect_entry.name == "vac_1_Cd_0": + if defect_entry.name in [ + "vac_1_Cd_0", + "as_1_Te_on_Cd_0", + "Int_Te_3_unperturbed_0", + "as_2_Te_on_Cd_C3v_metastable_1", + ]: assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "Cs" - elif defect_entry.name == "vac_1_Cd_Td_0": + elif defect_entry.name in [ + "vac_1_Cd_Td_0", + "as_1_Cd_on_Te_2", + ]: assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "D2d" - elif defect_entry.name == "vac_2_Te_orig_metastable_-1": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "C1" elif defect_entry.name == "vac_2_Te_orig_non_JT_distorted_0": assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "C3v" - elif defect_entry.name == "vac_2_Te_shaken_-2": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "C1" - elif defect_entry.name == "as_1_Cd_on_Te_2": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "D2d" - elif ( - defect_entry.name == "as_1_Cd_on_Te_1" or defect_entry.name == "as_2_Cd_on_Te_metastable_0" - ): - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "C1" - elif defect_entry.name == "as_1_Te_on_Cd_0": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "Cs" - elif defect_entry.name == "as_1_Te_on_Cd_-1": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "C1" - elif defect_entry.name == "as_2_Te_on_Cd_C3v_metastable_1": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "Cs" - elif ( - defect_entry.name == "Int_Te_3_C3v_meta_2" or defect_entry.name == "Int_Te_3_unperturbed_1" - ): + elif defect_entry.name in [ + "as_1_Cd_on_Te_1", + "as_2_Cd_on_Te_metastable_0", + "vac_2_Te_orig_metastable_-1", + "vac_2_Te_shaken_-2", + "Int_Te_3_C3v_meta_2", + "Int_Te_3_unperturbed_1", + ]: assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "C1" - elif defect_entry.name == "Int_Te_3_unperturbed_0": - assert point_symmetry(defect_entry.defect_supercell, symprec=0.01) == "Cs" else: assert ( point_symmetry(defect_entry.defect_supercell, symprec=0.01) @@ -1791,13 +1824,8 @@ def test_CdTe_all_intrinsic_defects(self): assert list(sym_degen_df.iloc[i]) == row # delete symmetry info to force re-parsing, to test symmetry/degeneracy functions - thermo_wout_symm_meta = deepcopy(cdte_defect_thermo) - for defect_entry in thermo_wout_symm_meta.defect_entries: - for key in list(defect_entry.calculation_metadata.keys()): - if "symmetry" in key or "degeneracy" in key: - del defect_entry.calculation_metadata[key] - - sym_degen_df = thermo_wout_symm_meta.get_symmetries_and_degeneracies() + self._clear_symmetry_degeneracy_info(cdte_defect_thermo) + sym_degen_df = cdte_defect_thermo.get_symmetries_and_degeneracies() for i, row in enumerate(cdte_sym_degen_lists): print(i, row) if row[0] == "Int_Te_3_C3v_meta": @@ -1959,15 +1987,6 @@ def test_Sb2O5_formation_energies(self): os.remove("test.csv") - def test_symmetry_degeneracy_unparsed(self): - """ - Test that the symmetry and degeneracy functions still work if the - symmetry and degeneracy metadata wasn't parsed earlier for any reason - (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 From 84ba57edeaaffc74d375b5def805c0c54eeee6c0 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Tue, 30 Jul 2024 17:00:54 -0400 Subject: [PATCH 02/17] Parametrize chemical potentials tests, to test each case with both the legacy and new MP APIs --- doped/chemical_potentials.py | 6 +- tests/test_chemical_potentials.py | 289 +++++++++++++++++++----------- 2 files changed, 184 insertions(+), 111 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index 5cce5a1a..a0eb7831 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -845,12 +845,12 @@ def __init__( """ self.e_above_hull = e_above_hull # store parameters for reference self.full_phase_diagram = full_phase_diagram - # get API key, and print info message if it corresponds to legacy MP -- remove this (and legacy - # MP API warning filter) in future versions (TODO) + # get API key, and print info message if it corresponds to legacy MP -- remove this and legacy + # MP API warning filter in future versions (TODO) self.api_key, self.legacy_MP = _parse_MP_API_key(api_key, legacy_MP_info=True) warnings.filterwarnings( # Remove in future when users have been given time to transition "ignore", message="You are using the legacy MPRester" - ) # currently rely on this so shouldn't show warning, `message` only needs to match start + ) # previously relied on this so shouldn't show warning, `message` only needs to match start # TODO: Should hard code S (solid + S8), P, Te and Se in here too. Common anions with a # lot of unnecessary polymorphs on MP. Should at least scan over elemental phases and hard code diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index 675c0fb4..59c5102c 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -4,8 +4,11 @@ import os import shutil +import sys import unittest import warnings +from functools import wraps +from io import StringIO from pathlib import Path import numpy as np @@ -26,45 +29,77 @@ def _compare_chempot_dicts(dict1, dict2): assert np.isclose(val, dict2[key], atol=1e-5) -class CompetingPhases(unittest.TestCase): +def parameterized_subtest(api_key_dict=None): + """ + A test decorator to allow running competing phases tests with both the + legacy and new Materials Project API keys. + """ + if api_key_dict is None: # set to SK MP Imperial email (GitHub) A/C keys by default + api_key_dict = {"legacy": "c2LiJRMiBeaN5iXsH", "new": "UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv"} + + def decorator(test_func): + @wraps(test_func) + def wrapper(self, *args, **kwargs): + for name, api_key in api_key_dict.items(): + with self.subTest(api_key=api_key): + print(f"Testing with {name} API") + test_func(self, api_key, *args, **kwargs) + + return wrapper + + return decorator + + +class CompetingPhasesTestCase(unittest.TestCase): def setUp(self) -> None: self.path = Path(__file__).parents[0] - self.api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email A/C + self.legacy_api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email (GitHub) A/C + self.api_key = "UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv" # SK MP Imperial email (GitHub) A/C def tearDown(self) -> None: if Path("competing_phases").is_dir(): shutil.rmtree("competing_phases") - def test_init(self): - cp = chemical_potentials.CompetingPhases("ZrO2", e_above_hull=0.03, api_key=self.api_key) + @parameterized_subtest() + def test_init(self, api_key): + cp = chemical_potentials.CompetingPhases("ZrO2", e_above_hull=0.03, api_key=api_key) + assert len(cp.entries) == 13 - assert cp.entries[0].name == "O2" + assert [entry.name for entry in cp.entries] == [ + "O2", + "Zr", + "Zr3O", + "ZrO2", + "Zr3O", + "Zr3O", + "Zr2O", + "ZrO2", + "ZrO2", + "Zr", + "ZrO2", + "ZrO2", + "ZrO2", + ] assert cp.entries[0].data["total_magnetization"] == 2 - assert cp.entries[0].data["e_above_hull"] == 0 - assert cp.entries[0].data["molecule"] + for i, entry in enumerate(cp.entries): + print(entry.name, entry.energy) + if i < 4: + assert entry.data.get(cp.property_key_dict["energy_above_hull"]) == 0 + else: + assert entry.data.get(cp.property_key_dict["energy_above_hull"]) > 0 + + if entry.name == "O2": + assert entry.data["molecule"] + else: + assert not entry.data["molecule"] + assert np.isclose(cp.entries[0].data["energy_per_atom"], -4.94795546875) assert np.isclose(cp.entries[0].data["energy"], -9.8959109375) - assert cp.entries[1].name == "Zr" assert np.isclose(cp.entries[1].data["total_magnetization"], 0, atol=1e-3) - assert cp.entries[1].data["e_above_hull"] == 0 - assert not cp.entries[1].data["molecule"] - assert cp.entries[2].name == "Zr3O" - assert cp.entries[2].data["e_above_hull"] == 0 - assert cp.entries[3].name == "ZrO2" - assert cp.entries[3].data["e_above_hull"] == 0 - assert cp.entries[4].name == "Zr3O" - assert cp.entries[5].name == "Zr3O" - assert cp.entries[6].name == "Zr2O" - assert cp.entries[7].name == "ZrO2" - assert cp.entries[8].name == "ZrO2" - assert cp.entries[9].name == "Zr" - assert cp.entries[10].name == "ZrO2" - assert cp.entries[11].name == "ZrO2" - assert cp.entries[12].name == "ZrO2" - - assert "Zr4O" not in [e.name for e in cp.entries] - - def test_init_ZnSe(self): + assert "Zr4O" not in [e.name for e in cp.entries] # not bordering or potentially with EaH + + @parameterized_subtest() + def test_init_ZnSe(self, api_key): """ As noted by Savya Aggarwal, the legacy MP API code didn't return ZnSe2 as a competing phase despite being on the hull and bordering ZnSe, @@ -76,54 +111,64 @@ def test_init_ZnSe(self): Updated code which re-calculates the energy above hull avoids this issue. """ - cp = chemical_potentials.CompetingPhases("ZnSe", api_key=self.api_key) + cp = chemical_potentials.CompetingPhases("ZnSe", api_key=api_key) assert any(e.name == "ZnSe2" for e in cp.entries) assert len(cp.entries) == 14 # ZnSe2 now present znse2_entry = next(e for e in cp.entries if e.name == "ZnSe2") - assert znse2_entry.data["e_above_hull"] == 0 + assert znse2_entry.data.get(cp.property_key_dict["energy_above_hull"]) == 0 assert not znse2_entry.data["molecule"] assert np.isclose(znse2_entry.data["energy_per_atom"], -3.080017) assert np.isclose(znse2_entry.data["energy"], -3.080017 * 12) - def test_init_full_phase_diagram(self): + @parameterized_subtest() + def test_init_full_phase_diagram(self, api_key): cp = chemical_potentials.CompetingPhases( - "ZrO2", e_above_hull=0.03, api_key=self.api_key, full_phase_diagram=True + "ZrO2", e_above_hull=0.03, api_key=api_key, full_phase_diagram=True ) + assert len(cp.entries) == 14 # Zr4O now present - assert cp.entries[0].name == "O2" + assert [entry.name for entry in cp.entries] == [ + "O2", + "Zr", + "Zr3O", + "Zr4O", + "ZrO2", + "Zr3O", + "Zr3O", + "Zr2O", + "ZrO2", + "ZrO2", + "Zr", + "ZrO2", + "ZrO2", + "ZrO2", + ] assert cp.entries[0].data["total_magnetization"] == 2 - assert cp.entries[0].data["e_above_hull"] == 0 - assert cp.entries[0].data["molecule"] + for i, entry in enumerate(cp.entries): + print(entry.name, entry.energy) + if i < 5: + assert entry.data.get(cp.property_key_dict["energy_above_hull"]) == 0 + else: + assert entry.data.get(cp.property_key_dict["energy_above_hull"]) > 0 + + if entry.name == "O2": + assert entry.data["molecule"] + else: + assert not entry.data["molecule"] + assert np.isclose(cp.entries[0].data["energy_per_atom"], -4.94795546875) assert np.isclose(cp.entries[0].data["energy"], -9.8959109375) - assert cp.entries[1].name == "Zr" assert np.isclose(cp.entries[1].data["total_magnetization"], 0, atol=1e-3) - assert cp.entries[1].data["e_above_hull"] == 0 - assert not cp.entries[1].data["molecule"] - assert cp.entries[2].name == "Zr3O" - assert cp.entries[2].data["e_above_hull"] == 0 - assert cp.entries[3].name == "Zr4O" # new entry! - assert cp.entries[3].data["e_above_hull"] == 0 - assert cp.entries[4].name == "ZrO2" - assert cp.entries[4].data["e_above_hull"] == 0 - assert cp.entries[5].name == "Zr3O" - assert cp.entries[6].name == "Zr3O" - assert cp.entries[7].name == "Zr2O" - assert cp.entries[8].name == "ZrO2" - assert cp.entries[9].name == "ZrO2" - assert cp.entries[10].name == "Zr" - assert cp.entries[11].name == "ZrO2" - assert cp.entries[12].name == "ZrO2" - assert cp.entries[13].name == "ZrO2" - - def test_init_ytos(self): + + @parameterized_subtest() + def test_init_ytos(self, api_key): # 144 phases on Y-Ti-O-S MP phase diagram - cp = chemical_potentials.CompetingPhases("Y2Ti2S2O5", e_above_hull=0.1, api_key=self.api_key) + cp = chemical_potentials.CompetingPhases("Y2Ti2S2O5", e_above_hull=0.1, api_key=api_key) assert len(cp.entries) == 115 # 115 phases with default algorithm self.check_O2_entry(cp) cp = chemical_potentials.CompetingPhases( - "Y2Ti2S2O5", e_above_hull=0.1, full_phase_diagram=True, api_key=self.api_key + "Y2Ti2S2O5", e_above_hull=0.1, full_phase_diagram=True, api_key=api_key ) assert len(cp.entries) == 140 # 144 phases on Y-Ti-O-S MP phase diagram, 4 extra O2 phases removed self.check_O2_entry(cp) @@ -134,21 +179,22 @@ def check_O2_entry(self, cp): assert len(result) == 1 assert result[0].name == "O2" assert result[0].data["total_magnetization"] == 2 - assert result[0].data["e_above_hull"] == 0 + assert result[0].data[cp.property_key_dict["energy_above_hull"]] == 0 assert result[0].data["molecule"] assert np.isclose(result[0].data["energy_per_atom"], -4.94795546875) - def test_unstable_host(self): + @parameterized_subtest() + def test_unstable_host(self, api_key): """ Test generating CompetingPhases with a composition that's unstable on the Materials Project database. """ for cp_settings in [ - {"composition": "Na2FePO4F", "e_above_hull": 0.02, "api_key": self.api_key}, + {"composition": "Na2FePO4F", "e_above_hull": 0.02, "api_key": api_key}, { "composition": "Na2FePO4F", "e_above_hull": 0.02, - "api_key": self.api_key, + "api_key": api_key, "full_phase_diagram": True, }, ]: @@ -179,9 +225,9 @@ def test_unknown_host(self): Materials Project database. """ for cp_settings in [ - {"composition": "Cu2SiSe4", "api_key": self.api_key}, - {"composition": "Cu2SiSe4", "api_key": self.api_key, "e_above_hull": 0.0}, - {"composition": "Cu2SiSe4", "api_key": self.api_key, "full_phase_diagram": True}, + {"composition": "Cu2SiSe4", "api_key": self.legacy_api_key}, + {"composition": "Cu2SiSe4", "api_key": self.legacy_api_key, "e_above_hull": 0.0}, + {"composition": "Cu2SiSe4", "api_key": self.legacy_api_key, "full_phase_diagram": True}, ]: print(f"Testing with settings: {cp_settings}") with warnings.catch_warnings(record=True) as w: @@ -198,31 +244,9 @@ def test_unknown_host(self): else: assert len(cp.entries) == 38 - def test_api_keys_errors(self): - api_key_error_start = ValueError( - "The supplied API key (either ``api_key`` or 'PMG_MAPI_KEY' in ``~/.pmgrc.yaml`` or " - "``~/.config/.pmgrc.yaml``;" - ) - with pytest.raises(ValueError) as e: - chemical_potentials.CompetingPhases( - "ZrO2", - api_key="test", - ) - assert str(api_key_error_start) in str(e.value) - # - assert ( - "is not a valid Materials Project API " - "key, which is required by doped for competing phase generation. See the doped " - "installation instructions for details:\n" - "https://doped.readthedocs.io/en/latest/Installation.html#setup-potcars-and-materials" - "-project-api" - ) in str(e.value) - - # test all works fine with key from new MP API: - assert chemical_potentials.CompetingPhases("ZrO2", api_key="UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv") - - def test_convergence_setup(self): - cp = chemical_potentials.CompetingPhases("ZrO2", e_above_hull=0.03, api_key=self.api_key) + @parameterized_subtest() + def test_convergence_setup(self, api_key): + cp = chemical_potentials.CompetingPhases("ZrO2", e_above_hull=0.03, api_key=api_key) # potcar spec doesn't need potcars set up for pmg and it still works cp.convergence_setup(potcar_spec=True) assert len(cp.metals) == 6 @@ -247,8 +271,9 @@ def test_convergence_setup(self): assert any(line == "GGA = Ps\n" for line in contents) assert any(line == "NSW = 0\n" for line in contents) - def test_vasp_std_setup(self): - cp = chemical_potentials.CompetingPhases("ZrO2", e_above_hull=0.03, api_key=self.api_key) + @parameterized_subtest() + def test_vasp_std_setup(self, api_key): + cp = chemical_potentials.CompetingPhases("ZrO2", e_above_hull=0.03, api_key=api_key) cp.vasp_std_setup(potcar_spec=True) assert len(cp.nonmetals) == 6 assert len(cp.metals) == 6 @@ -283,36 +308,84 @@ def test_vasp_std_setup(self): assert np.isclose(struct.sites[0].frac_coords, [0.49983339, 0.5, 0.50016672]).all() assert np.isclose(struct.sites[1].frac_coords, [0.49983339, 0.5, 0.5405135]).all() + def test_api_keys_errors(self): + api_key_error_start = ValueError( + "The supplied API key (either ``api_key`` or 'PMG_MAPI_KEY' in ``~/.pmgrc.yaml`` or " + "``~/.config/.pmgrc.yaml``;" + ) + with pytest.raises(ValueError) as e: + chemical_potentials.CompetingPhases( + "ZrO2", + api_key="test", + ) + assert str(api_key_error_start) in str(e.value) + # + assert ( + "is not a valid Materials Project API " + "key, which is required by doped for competing phase generation. See the doped " + "installation instructions for details:\n" + "https://doped.readthedocs.io/en/latest/Installation.html#setup-potcars-and-materials" + "-project-api" + ) in str(e.value) + + # test all works fine with key from new MP API: + assert chemical_potentials.CompetingPhases("ZrO2", api_key="UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv") + + def test_legacy_API_message(self): + """ + Quick test to check that the message about doped now supporting the new + Materials Project API is printed to stdout as expected. + """ + original_stdout = sys.stdout # Save a reference to the original standard output + sys.stdout = StringIO() # Redirect standard output to a stringIO object. + + try: + chemical_potentials.CompetingPhases("Si", api_key=self.legacy_api_key) + output = sys.stdout.getvalue() # Return a str containing the printed output + finally: + sys.stdout = original_stdout # Reset standard output to its original value. + + print(output) # for debugging + assert ( + "Note that doped now supports the new Materials Project API, which can be used by updating " + "your API key in ~/.pmgrc.yaml or ~/.config/.pmgrc.yaml: " + "https://doped.readthedocs.io/en/latest/Installation.html#setup-potcars-and-materials" + "-project-api" + ) in output -class ExtrinsicCompetingPhasesTest(unittest.TestCase): + +class ExtrinsicCompetingPhasesTestCase(unittest.TestCase): + # TODO: Merge with CompetingPhases tests once actual classes merged # TODO: Need to add tests for co-doping, full_sub_approach, full_phase_diagram etc!! def setUp(self) -> None: self.path = Path(__file__).parents[0] - self.api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email A/C - self.ex_cp = chemical_potentials.ExtrinsicCompetingPhases( - "ZrO2", extrinsic_species="La", e_above_hull=0, api_key=self.api_key - ) + self.legacy_api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email (GitHub) A/C + self.api_key = "UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv" # SK MP Imperial email (GitHub) A/C def tearDown(self) -> None: if Path("competing_phases").is_dir(): shutil.rmtree("competing_phases") - def test_init(self): - assert len(self.ex_cp.entries) == 2 - assert self.ex_cp.entries[0].name == "La" # definite ordering - assert self.ex_cp.entries[1].name == "La2Zr2O7" # definite ordering - assert [(entry.data["e_above_hull"] == 0) for entry in self.ex_cp.entries] + @parameterized_subtest() + def test_init(self, api_key): + ex_cp = chemical_potentials.ExtrinsicCompetingPhases( + "ZrO2", extrinsic_species="La", e_above_hull=0, api_key=api_key + ) + assert len(ex_cp.entries) == 2 + assert ex_cp.entries[0].name == "La" # definite ordering + assert ex_cp.entries[1].name == "La2Zr2O7" # definite ordering + assert [(entry.data["e_above_hull"] == 0) for entry in ex_cp.entries] # names of intrinsic entries: ['O2', 'Zr', 'Zr3O', 'ZrO2'] - assert len(self.ex_cp.intrinsic_entries) == 4 - assert self.ex_cp.intrinsic_entries[0].name == "O2" - assert self.ex_cp.intrinsic_entries[1].name == "Zr" - assert self.ex_cp.intrinsic_entries[2].name == "Zr3O" - assert self.ex_cp.intrinsic_entries[3].name == "ZrO2" - assert all(entry.data["e_above_hull"] == 0 for entry in self.ex_cp.intrinsic_entries) + assert len(ex_cp.intrinsic_entries) == 4 + assert ex_cp.intrinsic_entries[0].name == "O2" + assert ex_cp.intrinsic_entries[1].name == "Zr" + assert ex_cp.intrinsic_entries[2].name == "Zr3O" + assert ex_cp.intrinsic_entries[3].name == "ZrO2" + assert all(entry.data["e_above_hull"] == 0 for entry in ex_cp.intrinsic_entries) cp = chemical_potentials.ExtrinsicCompetingPhases( - "ZrO2", extrinsic_species="La", api_key=self.api_key + "ZrO2", extrinsic_species="La", api_key=api_key ) # default e_above_hull=0.1 assert len(cp.entries) == 5 assert cp.entries[2].name == "La" # definite ordering, same 1st & 2nd as before @@ -323,7 +396,7 @@ def test_init(self): assert len(cp.intrinsic_entries) == 28 -class ChemPotAnalyzerTest(unittest.TestCase): +class ChemPotAnalyzerTestCase(unittest.TestCase): def setUp(self): self.path = Path(__file__).parents[1].joinpath("examples/competing_phases") self.stable_system = "ZrO2" From f750b7c773ddb6203060d97f609f7180ab9f3cb7 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 10:57:06 -0400 Subject: [PATCH 03/17] Update paper list --- README.md | 2 +- docs/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0769ebf5..1f95a9c4 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ Not currently cited but should be for chempot plot: --> - Z. Yuan & G. Hautier **_First-principles study of defects and doping limits in CaO_** [_Applied Physics Letters_](https://doi.org/10.1063/5.0211707) 2024 - B. E. Murdock et al. **_Li-Site Defects Induce Formation of Li-Rich Impurity Phases: Implications for Charge Distribution and Performance of LiNi0.5-xMxMn1.5O4 Cathodes (M = Fe and Mg; x = 0.05–0.2)_** [_Advanced Materials_](https://doi.org/10.1002/adma.202400343) 2024 -- A. G. Squires et al. **_Oxygen dimerization as a defect-driven process in bulk LiNiO22_** [_ChemRxiv_](https://doi.org/10.26434/chemrxiv-2024-lcmkj) 2024 +- A. G. Squires et al. **_Oxygen dimerization as a defect-driven process in bulk LiNiO22_** [_ACS Energy Letters_](https://pubs.acs.org/doi/10.1021/acsenergylett.4c01307) 2024 - Y. Fu & H. Lohan et al. **_Factors Enabling Delocalized Charge-Carriers in Pnictogen-Based Solar Absorbers: In-depth Investigation into CuSbSe2_** [_arXiv_](https://doi.org/10.48550/arXiv.2401.02257) 2024 - S. Hachmioune et al. **_Exploring the Thermoelectric Potential of MgB4: Electronic Band Structure, Transport Properties, and Defect Chemistry_** [_Chemistry of Materials_](https://doi.org/10.1021/acs.chemmater.4c00584) 2024 diff --git a/docs/index.rst b/docs/index.rst index 885300a1..971320f3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -94,7 +94,7 @@ 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₂** `ACS Energy Letters `__ 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 From f2f3293d8795695710a8b81178adc3b7a6d5dbc8 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 11:50:10 -0400 Subject: [PATCH 04/17] Update paper list again --- README.md | 1 + docs/index.rst | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f95a9c4..6fd58db1 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ As shown in the `doped` tutorials, it is highly recommended to use the [`ShakeNB Not currently cited but should be for chempot plot: - Quadir et al. **_Low-Temperature Synthesis of Stable CaZn2P2 Zintl Phosphide Thin Films as Candidate Top Absorbers_** [_arXiv_](https://arxiv.org/abs/2406.15637) 2024 --> +- M. Elgaml et al. **_Controlling the Superconductivity of Nb2PdxS5 via Reversible Li Intercalation_** [_Inorganic Chemistry_](https://pubs.acs.org/doi/full/10.1021/acs.inorgchem.3c03524) 2024 - Z. Yuan & G. Hautier **_First-principles study of defects and doping limits in CaO_** [_Applied Physics Letters_](https://doi.org/10.1063/5.0211707) 2024 - B. E. Murdock et al. **_Li-Site Defects Induce Formation of Li-Rich Impurity Phases: Implications for Charge Distribution and Performance of LiNi0.5-xMxMn1.5O4 Cathodes (M = Fe and Mg; x = 0.05–0.2)_** [_Advanced Materials_](https://doi.org/10.1002/adma.202400343) 2024 - A. G. Squires et al. **_Oxygen dimerization as a defect-driven process in bulk LiNiO22_** [_ACS Energy Letters_](https://pubs.acs.org/doi/10.1021/acsenergylett.4c01307) 2024 diff --git a/docs/index.rst b/docs/index.rst index 971320f3..83e5c7e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -92,7 +92,8 @@ Studies using ``doped``, so far .. Not currently cited but should be for chempot plot: Quadir et al. **_Low-Temperature Synthesis of Stable CaZn2P2 Zintl Phosphide Thin Films as Candidate Top Absorbers_** [_arXiv_](https://arxiv.org/abs/2406.15637) 2024 -- Z\. Yuan & G. Hautier **First-principles study of defects and doping limits in CaO** `Applied Physics Letters `_ 2024 +- M\. Elgaml et al. **Controlling the Superconductivity of Nb** :sub:`2` **Pd** :sub:`x` **S** :sub:`5` **via Reversible Li Intercalation** `Inorganic Chemistry `__ 2024 +- 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₂** `ACS Energy Letters `__ 2024 - Y\. Fu & H. Lohan et al. **Factors Enabling Delocalized Charge-Carriers in Pnictogen-Based Solar Absorbers: In-depth Investigation into CuSbSe2** `arXiv `_ 2024 From 69e08fe1f983a08cce55e60cce333d9e8c0901da Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 16:39:38 -0400 Subject: [PATCH 05/17] Make MP querying and border candidate pruning significantly more efficient, faster competing phases generation (~2-5x speedup) --- doped/chemical_potentials.py | 174 ++++++++++++++++-------------- tests/test_chemical_potentials.py | 17 +-- 2 files changed, 102 insertions(+), 89 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index a0eb7831..1979bc31 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -52,13 +52,15 @@ "icsd_id", "icsd_ids", # some entries have icsd_id and some have icsd_ids "theoretical", - "formation_energy_per_atom", - "energy_per_atom", - "energy", + "formation_energy_per_atom", # uncorrected with legacy MP API, corrected with new API + "energy_per_atom", # note that with legacy MP API this is uncorrected, but is corrected with new API + "energy", # note that with legacy MP API this is uncorrected, but is corrected with new API "total_magnetization", "nelements", "elements", -] +] # note that, because the energy values in the ``data`` dict are uncorrected with legacy MP API and +# corrected with the new MP API, we should refrain from using these values when possible. The ``energy`` +# and ``energy_per_atom`` attributes are consistent (corrected in both cases) MP_API_property_keys = { "legacy": { @@ -78,6 +80,14 @@ # pymatgen/analysis/defects/plotting/phases.py may be useful +def _get_pretty_formula(entry_data: dict): + return entry_data.get("pretty_formula", entry_data.get("formula_pretty", "N/A")) + + +def _get_e_above_hull(entry_data: dict): + return entry_data.get("e_above_hull", entry_data.get("energy_above_hull", 0.0)) + + def make_molecule_in_a_box(element: str): """ Generate an X2 'molecule-in-a-box' structure for the input element X, (i.e. @@ -141,7 +151,7 @@ def make_molecular_entry(computed_entry, legacy_MP=False): """ property_key_dict = MP_API_property_keys["legacy"] if legacy_MP else MP_API_property_keys["new"] assert len(computed_entry.composition.elements) == 1 # Elemental! - formula = computed_entry.data[property_key_dict["pretty_formula"]] + formula = _get_pretty_formula(computed_entry.data) element = formula[0].upper() struct, total_magnetization = make_molecule_in_a_box(element) molecular_entry = ComputedStructureEntry( @@ -430,14 +440,11 @@ def get_entries_in_chemsys( if e_above_hull is not None: MP_full_pd_entries = [ - entry - for entry in MP_full_pd_entries - if entry.data[property_key_dict["energy_above_hull"]] <= e_above_hull + entry for entry in MP_full_pd_entries if _get_e_above_hull(entry.data) <= e_above_hull ] - MP_full_pd_entries.sort( # sort by energy above hull, num_species, then alphabetically: - key=lambda x: _entries_sorting_func(x, legacy_MP) - ) + # sort by energy above hull, num_species, then alphabetically: + MP_full_pd_entries.sort(key=lambda x: _entries_sorting_func(x)) if return_all_info: return MP_full_pd_entries, property_key_dict, property_data_fields @@ -491,9 +498,8 @@ def get_entries( **kwargs, ) - entries.sort( # sort by energy above hull, num_species, then alphabetically: - key=lambda x: _entries_sorting_func(x, legacy_MP) - ) + # sort by energy above hull, num_species, then alphabetically: + entries.sort(key=lambda x: _entries_sorting_func(x)) return entries @@ -569,15 +575,16 @@ def _parse_MP_API_key(api_key: Optional[str] = None, legacy_MP_info: bool = Fals def get_MP_summary_docs( - chemsys: Union[str, list[str]], - api_key: Optional[str] = None, entries: Optional[list[ComputedEntry]] = None, + chemsys: Optional[Union[str, list[str]]] = None, + api_key: Optional[str] = None, data_fields: Optional[list[str]] = None, **kwargs, ): """ Get the corresponding Materials Project (MP) ``SummaryDoc`` documents for - computed entries in the input ``chemsys`` chemical system. + computed entries in the input ``entries`` list or ``chemsys`` chemical + system. If ``entries`` is provided (which should be a list of ``ComputedEntry``s from the Materials Project), then only ``SummaryDoc``s in this chemical @@ -589,16 +596,24 @@ def get_MP_summary_docs( from the corresponding ``SummaryDoc`` attribute to ``ComputedEntry.data`` for the matching ``ComputedEntry`` in ``entries`` - Note that this function can only be used with the new Materials Project API, as the legacy API does not have the ``SummaryDoc`` functionality (but most of the same data is available through the ``property_data`` arguments for the legacy-API-compatible functions). Args: + entries (list[ComputedEntry]): + Optional input; list of ``ComputedEntry`` objects for the input chemical + system. If provided, only ``SummaryDoc``s which match one of these entries + (based on the MPIDs given in ``ComputedEntry.entry_id``/ + ``ComputedEntry.data["material_id"]`` and ``SummaryDoc.material_id``) are + returned. Moreover, all data fields listed in ``data_fields`` will be copied + from the corresponding ``SummaryDoc`` attribute to ``ComputedEntry.data`` for + the matching ``ComputedEntry`` in ``entries``. chemsys (str, list[str]): - Chemical system to get entries for, in the format "A-B-C" or - ["A", "B", "C"]. E.g. "Li-Fe-O" or ["Li", "Fe", "O"]. + Optional input; chemical system to get entries for, in the format "A-B-C" or + ["A", "B", "C"]. E.g. "Li-Fe-O" or ["Li", "Fe", "O"]. Either ``entries`` or + ``chemsys`` must be provided! api_key (str): Materials Project (MP) API key, needed to access the MP database to obtain the corresponding ``SummaryDoc`` documents. Must be @@ -607,14 +622,6 @@ def get_MP_summary_docs( or ``~/.config/.pmgrc.yaml``) - see the ``doped`` Installation docs page: https://doped.readthedocs.io/en/latest/Installation.html#setup-potcars-and-materials -project-api - entries (list[ComputedEntry]): - Optional input; list of ``ComputedEntry`` objects for the input chemical - system. If provided, only ``SummaryDoc``s which match one of these entries - (based on the MPIDs given in ``ComputedEntry.entry_id``/ - ``ComputedEntry.data["material_id"]`` and ``SummaryDoc.material_id``) are - returned. Moreover, all data fields listed in ``data_fields`` will be copied - from the corresponding ``SummaryDoc`` attribute to ``ComputedEntry.data`` for - the matching ``ComputedEntry`` in ``entries``. data_fields (list[str]): List of data fields to copy from the corresponding ``SummaryDoc`` attributes to the ``ComputedEntry.data`` objects, if ``entries`` is supplied. @@ -633,23 +640,23 @@ def get_MP_summary_docs( "`get_MP_summary_docs` can only be used with the new Materials Project (MP) API (see " "https://next-gen.materialsproject.org/api), but a legacy MP API key was supplied!" ) + if entries is None and chemsys is None: + raise ValueError("Either `entries` or `chemsys` must be provided!") - with MPRester(api_key) as mpr: - MP_full_pd_docs = { - doc.material_id: doc - for doc in mpr.materials.summary.search( - chemsys=_get_all_chemsyses("-".join(chemsys)), **kwargs - ) + if entries: + summary_search_kwargs = { + "material_ids": [entry.data["material_id"] for entry in entries], + **kwargs, } + else: + assert chemsys is not None + summary_search_kwargs = {"chemsys": _get_all_chemsyses("-".join(chemsys)), **kwargs} - if not entries: - return MP_full_pd_docs + with MPRester(api_key) as mpr: + MP_docs = {doc.material_id: doc for doc in mpr.materials.summary.search(**summary_search_kwargs)} - MP_docs = { - material_id: doc - for material_id, doc in MP_full_pd_docs.items() - if material_id in [entry.data["material_id"] for entry in entries] - } + if not entries: + return MP_docs if data_fields is None: data_fields = ["band_gap", "total_magnetization", "database_IDs"] # ICSD IDs and possibly others @@ -676,22 +683,16 @@ def get_MP_summary_docs( return MP_docs -def _entries_sorting_func(entry: ComputedEntry, legacy_MP=False, use_e_per_atom: bool = False): +def _entries_sorting_func(entry: ComputedEntry, use_e_per_atom: bool = False): """ Function to sort ``ComputedEntry``s by energy above hull, then by the number of elements in the formula, then alphabetically by formula. - Usage: entries_list.sort(key=_entries_sorting_func) + Usage: ``entries_list.sort(key=_entries_sorting_func)`` Args: entry (ComputedEntry): ComputedEntry object to sort. - legacy_MP (bool): - If ``True``, use the legacy Materials Project property data fields - (i.e. ``"e_above_hull"``, ``"pretty_formula"`` etc.), rather than - the new Materials Project API format (``"energy_above_hull"``, - ``"formula_pretty"`` etc.). - Default is ``False``. use_e_per_atom (bool): If ``True``, sort by energy per atom rather than energy above hull. Default is ``False``. @@ -702,11 +703,7 @@ def _entries_sorting_func(entry: ComputedEntry, legacy_MP=False, use_e_per_atom: in the formula, and formula name of the entry. """ return ( - ( - entry.energy_per_atom - if use_e_per_atom - else entry.data[MP_API_property_keys["legacy" if legacy_MP else "new"]["energy_above_hull"]] - ), + entry.energy_per_atom if use_e_per_atom else _get_e_above_hull(entry.data), len(Composition(entry.name).as_dict()), entry.name, ) @@ -775,18 +772,40 @@ def prune_entries_to_border_candidates( bordering_entry.name for bordering_entry in bordering_entries ] # compositions which border the host with EaH=0, according to MP, so we include all phases with # these compositions up to EaH=e_above_hull (which we've already pruned to) + # for determining phases which alter the chemical potential limits when renormalised, only need to + # retain the EaH=0 entries from above, so we use this reduced PD to save compute time when looping + # below: + reduced_pd_entries = { + entry + for entry in bordering_entries + if entry.data.get("energy_above_hull", entry.data.get("e_above_hull", 0)) == 0 + } # then add any other phases that would border the host material on the phase diagram, if their # relative energy was downshifted by ``e_above_hull``: - for entry in entries: # only check if not already bordering; can just use names for this: - if entry.name not in bordering_entry_names: + # only check if not already bordering; can just use names for this: + entries_to_test = [entry for entry in entries if entry.name not in bordering_entry_names] + entries_to_test.sort(key=_entries_sorting_func) # sort by energy above hull + # to save unnecessary looping, whenever we encounter a phase that is not being added to the border + # candidates list, skip all following phases with this composition (because they have higher + # energies above hull (because we've sorted by this) and so will also not border the host): + compositions_to_skip = [] + for entry in entries_to_test: + if entry.name not in compositions_to_skip: # decrease entry energy per atom by ``e_above_hull`` eV/atom renormalised_entry = _renormalise_entry(entry, e_above_hull) - new_phase_diagram = PhaseDiagram([*phase_diagram.entries, renormalised_entry]) + new_phase_diagram = PhaseDiagram( + [*reduced_pd_entries, bulk_computed_entry, renormalised_entry] + ) shifted_MP_chempots = get_chempots_from_phase_diagram(bulk_computed_entry, new_phase_diagram) + shifted_MP_bordering_phases = { + phase for limit in shifted_MP_chempots for phase in limit.split("-") + } - if shifted_MP_chempots != MP_chempots: # new bordering phase, add to list + if shifted_MP_bordering_phases != MP_bordering_phases: # new bordering phase, add to list bordering_entries.append(entry) + else: + compositions_to_skip.append(entry.name) return bordering_entries @@ -887,7 +906,7 @@ def __init__( # get all entries in the chemical system: self.MP_full_pd_entries, self.property_key_dict, self.property_data_fields = ( - get_entries_in_chemsys( + get_entries_in_chemsys( # get all entries in the chemical system, with EaH<``e_above_hull`` self.chemsys, api_key=self.api_key, e_above_hull=self.e_above_hull, @@ -896,7 +915,7 @@ def __init__( ) self.MP_full_pd = PhaseDiagram(self.MP_full_pd_entries) - # convert any gaseous elemental entries to molecules in a box, and prune to a_above_hull range + # convert any gaseous elemental entries to molecules in a box formatted_entries = self._generate_elemental_diatomic_phases(self.MP_full_pd_entries) # get bulk entry, and warn if not stable or not present on MP database: @@ -904,7 +923,7 @@ def __init__( entry for entry in formatted_entries # sorted by e_above_hull above in get_entries_in_chemsys if entry.composition.reduced_composition == self.bulk_composition.reduced_composition - and entry.data[self.property_key_dict["energy_above_hull"]] == 0.0 + and _get_e_above_hull(entry.data) == 0.0 ]: bulk_computed_entry = bulk_entries[0] # lowest energy entry for bulk (after sorting) else: # no EaH=0 bulk entries in pruned phase diagram, check first if present (but unstable) @@ -971,15 +990,13 @@ def __init__( else: # self.full_phase_diagram = True self.entries = formatted_entries - self.entries.sort( # sort by energy above hull, num_species, then alphabetically - key=lambda x: _entries_sorting_func(x, self.legacy_MP) - ) + # sort by energy above hull, num_species, then alphabetically: + self.entries.sort(key=lambda x: _entries_sorting_func(x)) if not self.legacy_MP: # need to pull ``SummaryDoc``s to get band_gap and magnetization info self.MP_docs = get_MP_summary_docs( - self.chemsys, - api_key=self.api_key, entries=self.entries, # sets "band_gap", "total_magnetization" and "database_IDs" fields + api_key=self.api_key, ) # TODO: Return dict of DictSet objects for this and vasp_std_setup() functions, as well as @@ -1094,7 +1111,7 @@ def convergence_setup( dict_set.write_input(fname, **kwargs) def _competing_phase_name(self, entry): - rounded_eah = round(entry.data[self.property_key_dict["energy_above_hull"]], 4) + rounded_eah = round(_get_e_above_hull(entry.data), 4) if np.isclose(rounded_eah, 0): return f"{entry.name}_EaH_0" return f"{entry.name}_EaH_{rounded_eah}" @@ -1271,25 +1288,23 @@ def _generate_elemental_diatomic_phases(self, entries: list[ComputedEntry]): for entry in entries.copy(): if ( - entry.data[self.property_key_dict["pretty_formula"]] in elemental_diatomic_gases - and entry.data[self.property_key_dict["energy_above_hull"]] == 0.0 + _get_pretty_formula(entry.data) in elemental_diatomic_gases + and _get_e_above_hull(entry.data) == 0.0 ): # only once for each matching gaseous elemental entry molecular_entry = make_molecular_entry(entry, legacy_MP=self.legacy_MP) if not any( entry.data["molecule"] - and entry.data[self.property_key_dict["pretty_formula"]] - == molecular_entry.data[self.property_key_dict["pretty_formula"]] + and _get_pretty_formula(entry.data) == _get_pretty_formula(molecular_entry.data) for entry in formatted_entries ): # first entry only entries.append(molecular_entry) formatted_entries.append(molecular_entry) - elif entry.data[self.property_key_dict["pretty_formula"]] not in elemental_diatomic_gases: + elif _get_pretty_formula(entry.data) not in elemental_diatomic_gases: entry.data["molecule"] = False formatted_entries.append(entry) - formatted_entries.sort( # sort by energy above hull, num_species, then alphabetically - key=lambda x: _entries_sorting_func(x, self.legacy_MP) - ) + # sort by energy above hull, num_species, then alphabetically: + formatted_entries.sort(key=lambda x: _entries_sorting_func(x)) return formatted_entries @@ -1520,13 +1535,10 @@ def __init__( self.entries += single_bordering_sub_el_entries - self.MP_full_pd_entries.sort( # sort by energy above hull, num_species, then alphabetically - key=lambda x: _entries_sorting_func(x, self.legacy_MP) - ) + # sort all entries by energy above hull, num_species, then alphabetically: + self.MP_full_pd_entries.sort(key=lambda x: _entries_sorting_func(x)) self.MP_full_pd = PhaseDiagram(self.MP_full_pd_entries) - self.entries.sort( # sort by energy above hull, num_species, then alphabetically - key=lambda x: _entries_sorting_func(x, self.legacy_MP) - ) + self.entries.sort(key=lambda x: _entries_sorting_func(x)) class CompetingPhasesAnalyzer: diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index 59c5102c..c5d4caba 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -113,12 +113,12 @@ def test_init_ZnSe(self, api_key): """ cp = chemical_potentials.CompetingPhases("ZnSe", api_key=api_key) assert any(e.name == "ZnSe2" for e in cp.entries) - assert len(cp.entries) == 14 # ZnSe2 now present + assert len(cp.entries) in {14, 16} # ZnSe2 present; 2 new Zn entries (mp-264...) with new MP API znse2_entry = next(e for e in cp.entries if e.name == "ZnSe2") assert znse2_entry.data.get(cp.property_key_dict["energy_above_hull"]) == 0 assert not znse2_entry.data["molecule"] - assert np.isclose(znse2_entry.data["energy_per_atom"], -3.080017) - assert np.isclose(znse2_entry.data["energy"], -3.080017 * 12) + assert np.isclose(znse2_entry.energy_per_atom, -3.394683861) + assert np.isclose(znse2_entry.energy, -3.394683861 * 12) @parameterized_subtest() def test_init_full_phase_diagram(self, api_key): @@ -161,16 +161,17 @@ def test_init_full_phase_diagram(self, api_key): assert np.isclose(cp.entries[1].data["total_magnetization"], 0, atol=1e-3) @parameterized_subtest() - def test_init_ytos(self, api_key): + def test_init_YTOS(self, api_key): # 144 phases on Y-Ti-O-S MP phase diagram cp = chemical_potentials.CompetingPhases("Y2Ti2S2O5", e_above_hull=0.1, api_key=api_key) - assert len(cp.entries) == 115 # 115 phases with default algorithm + assert len(cp.entries) in {109, 113} # legacy and new MP APIs self.check_O2_entry(cp) cp = chemical_potentials.CompetingPhases( "Y2Ti2S2O5", e_above_hull=0.1, full_phase_diagram=True, api_key=api_key ) - assert len(cp.entries) == 140 # 144 phases on Y-Ti-O-S MP phase diagram, 4 extra O2 phases removed + # 144/149 phases on Y-Ti-O-S legacy/new MP phase diagram, 4 extra O2 phases removed + assert len(cp.entries) in {140, 145} # legacy and new MP APIs self.check_O2_entry(cp) def check_O2_entry(self, cp): @@ -212,11 +213,11 @@ def test_unstable_host(self, api_key): "determine the possible competing phases with the same approach as usual", ]: print(sub_message) - assert sub_message in str(w[-1].message) + assert any(sub_message in str(warning.message) for warning in w) if cp_settings.get("full_phase_diagram"): assert len(cp.entries) == 128 else: - assert len(cp.entries) == 60 + assert len(cp.entries) == 50 self.check_O2_entry(cp) def test_unknown_host(self): From 48f7a4dffd30ab841d49f0a8c9b54bb60dd4a6ca Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 16:40:41 -0400 Subject: [PATCH 06/17] Update pymatgen requirement --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4d665b17..d291caae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,8 @@ dependencies = [ "tabulate", "matplotlib>=3.5.2", "numpy>=1.21.0", - "pymatgen>=2023.11.12", # limiting factor: POTCAR_STATS_PATH available from pymatgen.io.vasp.inputs + "pymatgen>=2024.5.31", # limiting factor: breaking changes in `VaspInputSet`; + # https://github.com/materialsproject/pymatgen/issues/3860 "spglib>=2.4.0", # previously explicitly set to <=2.0.2 to avoid unnecessary warnings # (https://github.com/spglib/spglib/issues/338) but causing dependency issues # (https://github.com/SMTG-Bham/doped/issues/73), being worked on in From a39e292e91f46c8bb30ab21305f583bede98546f Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 16:47:53 -0400 Subject: [PATCH 07/17] Update tests (new, stable entry for Na2FePO4F on new MP API) --- doped/chemical_potentials.py | 2 +- tests/test_chemical_potentials.py | 27 ++++++++++++++------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index 1979bc31..4d946f02 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -649,7 +649,7 @@ def get_MP_summary_docs( **kwargs, } else: - assert chemsys is not None + assert chemsys is not None # typing summary_search_kwargs = {"chemsys": _get_all_chemsyses("-".join(chemsys)), **kwargs} with MPRester(api_key) as mpr: diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index c5d4caba..36d61b8b 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -203,21 +203,22 @@ def test_unstable_host(self, api_key): with warnings.catch_warnings(record=True) as w: cp = chemical_potentials.CompetingPhases(**cp_settings) print([str(warning.message) for warning in w]) # for debugging - assert len([warning for warning in w if "You are using" not in str(warning.message)]) == 1 - for sub_message in [ - "Note that the Materials Project (MP) database entry for Na2FePO4F is not stable with " - "respect to competing phases, having an energy above hull of 0.1701 eV/atom.", - "Formally, this means that the host material is unstable and so has no chemical " - "potential limits; though in reality there may be errors in the MP energies", - "Here we downshift the host compound entry to the convex hull energy, and then " - "determine the possible competing phases with the same approach as usual", - ]: - print(sub_message) - assert any(sub_message in str(warning.message) for warning in w) + if len(api_key) != 32: # recalculated energy for Na2FePO4F on new MP API, now on hull + assert len([warning for warning in w if "You are using" not in str(warning.message)]) == 1 + for sub_message in [ + "Note that the Materials Project (MP) database entry for Na2FePO4F is not stable with " + "respect to competing phases, having an energy above hull of 0.1701 eV/atom.", + "Formally, this means that the host material is unstable and so has no chemical " + "potential limits; though in reality there may be errors in the MP energies", + "Here we downshift the host compound entry to the convex hull energy, and then " + "determine the possible competing phases with the same approach as usual", + ]: + print(sub_message) + assert any(sub_message in str(warning.message) for warning in w) if cp_settings.get("full_phase_diagram"): - assert len(cp.entries) == 128 + assert len(cp.entries) in {128, 173} # legacy, new MP APIs else: - assert len(cp.entries) == 50 + assert len(cp.entries) in {50, 68} # legacy, new MP APIs self.check_O2_entry(cp) def test_unknown_host(self): From 1a1e2db6a18246985f25e5ff2de33516e2eef834 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 16:52:33 -0400 Subject: [PATCH 08/17] Add `mp-api` to requirements for tests --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d291caae..4f8141e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,8 +46,9 @@ Repository = "https://github.com/SMTG-Bham/doped" [project.optional-dependencies] tests = [ - "pytest<8.2", # with pytest==8.2.0, seemed to cause memory issue on GH Actions mpl tests, python=3.10 - "pytest-mpl>=0.16.1" + "pytest>=8.3", # with pytest==8.2.0, seemed to cause memory issue on GH Actions mpl tests, python=3.10 + "pytest-mpl>=0.16.1", + "mp-api", # not required by default as user can use legacy API, but needed for tests ] docs = ["sphinx", "sphinx-book-theme", "sphinx_design"] analysis = [ From 9dd64a8e0c62c0839007ecaa7241de35e19d02dc Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 31 Jul 2024 17:15:12 -0400 Subject: [PATCH 09/17] Remove `spglib` requirement, now fixed as of v2.5 --- README.md | 6 ------ docs/Installation.rst | 16 ---------------- docs/Troubleshooting.rst | 6 +----- doped/__init__.py | 3 --- doped/analysis.py | 1 - doped/core.py | 10 ++-------- doped/generation.py | 2 -- doped/thermodynamics.py | 1 - doped/utils/symmetry.py | 21 +++++++++------------ pyproject.toml | 5 ----- 10 files changed, 12 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 6fd58db1..8d12f164 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,8 @@ displacements from DFT supercell calculations. See the [JOSS paper](https://doi. ## Installation ```bash pip install doped # install doped and dependencies -conda install -c conda-forge spglib # bundle C libraries with spglib ``` -Note that either `conda install -c conda-forge spglib` or -`pip install git+https://github.com/spglib/spglib --config-settings=cmake.define.SPGLIB_SHARED_LIBS=OFF` -should be used after `pip install doped`, which ensures that the correct C libraries are bundled with -`spglib`, to prevent unnecessary warnings. - Alternatively if desired, `doped` can also be installed from `conda` with: ```bash diff --git a/docs/Installation.rst b/docs/Installation.rst index 172f3ea8..00911c61 100644 --- a/docs/Installation.rst +++ b/docs/Installation.rst @@ -6,22 +6,6 @@ Installation .. code-block:: bash pip install doped # install doped and dependencies - conda install -c conda-forge spglib # bundle C libraries with spglib - -Note that either ``conda install -c conda-forge spglib`` or -``pip install git+https://github.com/spglib/spglib --config-settings=cmake.define.SPGLIB_SHARED_LIBS=OFF`` -should be used after ``pip install doped``, which ensures that the correct C libraries are bundled with -``spglib``, to prevent unnecessary warnings. You can check that the correct C libraries have been bundled -by running the following in Python, and confirming that the same version numbers are printed: - -.. code-block:: python - - import spglib - print(spglib.__version__) - print(spglib.spg_get_version_full()) - -In some cases, you might need to first uninstall ``spglib`` with both ``conda uninstall spglib`` and -``pip uninstall spglib`` (to ensure no duplicate installations), before reinstalling. Alternatively if desired, ``doped`` can also be installed from ``conda`` with: diff --git a/docs/Troubleshooting.rst b/docs/Troubleshooting.rst index 18f3b0f0..7adb1703 100644 --- a/docs/Troubleshooting.rst +++ b/docs/Troubleshooting.rst @@ -76,11 +76,7 @@ A known issue with ``spglib`` is that it can give unnecessary errors or warnings spglib: get_bravais_exact_positions_and_lattice failed spglib: ref_get_exact_structure_and_symmetry failed. -This can be fixed by reinstalling ``spglib`` with ``conda install -c conda-forge spglib`` or -``pip install git+https://github.com/spglib/spglib --config-settings=cmake.define.SPGLIB_SHARED_LIBS=OFF`` -as detailed in the `Installation `__ instructions. -This ensures the correct C libraries are bundled with ``spglib``. - +Typically this can be fixed by updating to ``spglib>=2.5`` with `pip install --upgrade spglib``. .. see doped_spglib_warnings.ipynb ``ShakeNBreak`` diff --git a/doped/__init__.py b/doped/__init__.py index 3e165202..632c0cd6 100644 --- a/doped/__init__.py +++ b/doped/__init__.py @@ -71,9 +71,6 @@ def _ignore_pmg_warnings(): # ignore warning about structure charge that appears when getting Vasprun.as_dict(): warnings.filterwarnings("ignore", message="Structure charge") - # SpglibDataset warning introduced in v2.4.1 - warnings.filterwarnings("ignore", message="dict interface") - _ignore_pmg_warnings() diff --git a/doped/analysis.py b/doped/analysis.py index bf524076..5ad8838e 100644 --- a/doped/analysis.py +++ b/doped/analysis.py @@ -223,7 +223,6 @@ def defect_from_structures( Dictionary of bulk supercell Voronoi node information, for further expedited site-matching. """ - warnings.filterwarnings("ignore", "dict interface") # ignore spglib warning from v2.4.1 try: def_type, comp_diff = get_defect_type_and_composition_diff(bulk_supercell, defect_supercell) except RuntimeError as exc: diff --git a/doped/core.py b/doped/core.py index 94609c24..fae61ad4 100644 --- a/doped/core.py +++ b/doped/core.py @@ -30,10 +30,6 @@ from easyunfold.procar import Procar as EasyunfoldProcar -# SpglibDataset warning introduced in v2.4.1, can be ignored for now -warnings.filterwarnings("ignore", message="dict interface") - - _orientational_degeneracy_warning = ( "The defect supercell has been detected to possibly have a non-scalar matrix expansion, " "which could be breaking the cell periodicity and possibly preventing the correct _relaxed_ " @@ -1378,10 +1374,8 @@ def _guess_and_set_struct_oxi_states(structure): if oxidation states could not be guessed. """ bv_analyzer = BVAnalyzer() - with contextlib.suppress(ValueError), warnings.catch_warnings(): - # ValueError raised if oxi states can't be assigned, and SpglibDataset warning introduced in - # v2.4.1, can be ignored for now: - warnings.filterwarnings("ignore", message="dict interface") + with contextlib.suppress(ValueError): + # ValueError raised if oxi states can't be assigned oxi_dec_structure = bv_analyzer.get_oxi_state_decorated_structure(structure) if all( np.isclose(int(specie.oxi_state), specie.oxi_state) for specie in oxi_dec_structure.species diff --git a/doped/generation.py b/doped/generation.py index 7df99fac..e3ffc3ad 100644 --- a/doped/generation.py +++ b/doped/generation.py @@ -1197,8 +1197,6 @@ class (such as ``clustering_tol``, ``stol``, ``min_dist`` etc), or to ``DefectsGenerator`` input parameters are also set as attributes. """ - # SpglibDataset warning introduced in v2.4.1, can later remove when pymatgen updated to avoid this: - warnings.filterwarnings("ignore", message="dict interface") self.defects: dict[str, list[Defect]] = {} # {defect_type: [Defect, ...]} self.defect_entries: dict[str, DefectEntry] = {} # {defect_species: DefectEntry} self.structure = structure diff --git a/doped/thermodynamics.py b/doped/thermodynamics.py index 0892dd63..75b0694d 100644 --- a/doped/thermodynamics.py +++ b/doped/thermodynamics.py @@ -266,7 +266,6 @@ def group_defects_by_distance( # would be independent of this... but challenging to setup without complex clustering approaches ( # for now this works very well as is, and this is a rare case and usually not a problem anyway as # dist_tol can just be adjusted as needed) - warnings.filterwarnings("ignore", "dict interface") # ignore spglib warning from v2.4.1 # initial group by Defect.name (same nominal defect), then distance to equiv sites # first make dictionary of nominal defect name: list of entries with that name diff --git a/doped/utils/symmetry.py b/doped/utils/symmetry.py index a0c77e8c..838067e3 100644 --- a/doped/utils/symmetry.py +++ b/doped/utils/symmetry.py @@ -26,8 +26,6 @@ _get_unrelaxed_defect_structure, ) -warnings.filterwarnings("ignore", "dict interface") # ignore spglib warning from v2.4.1 - def _set_spglib_warnings_env_var(): """ @@ -50,13 +48,15 @@ def _check_spglib_version(): warnings.warn( f"Your spglib Python version (spglib.__version__ = {python_version}) does not match its C " f"library version (spglib.spg_get_version_full() = {c_version}). This can lead to unnecessary " - f"spglib warning messages, but can be avoided by:\n" - f"- First uninstalling spglib with both `conda uninstall spglib` and `pip uninstall spglib` " - f"(to ensure no duplicate installations).\n" - f"- Then, install spglib with `conda install -c conda-forge spglib` or " - f"`pip install git+https://github.com/spglib/spglib " - f"--config-settings=cmake.define.SPGLIB_SHARED_LIBS=OFF` as detailed in the doped " - f"installation instructions: https://doped.readthedocs.io/en/latest/Installation.html" + f"spglib warning messages, but can be avoided by upgrading spglib with `pip install --upgrade " + f"spglib`." + # No longer required as of spglib v2.5: + # f"- First uninstalling spglib with both `conda uninstall spglib` and `pip uninstall spglib` " + # f"(to ensure no duplicate installations).\n" + # f"- Then, install spglib with `conda install -c conda-forge spglib` or " + # f"`pip install git+https://github.com/spglib/spglib " + # f"--config-settings=cmake.define.SPGLIB_SHARED_LIBS=OFF` as detailed in the doped " + # f"installation instructions: https://doped.readthedocs.io/en/latest/Installation.html" ) @@ -1100,7 +1100,6 @@ def point_symmetry_from_defect(defect, symm_ops=None, symprec=0.01): Returns: str: Defect point symmetry. """ - warnings.filterwarnings("ignore", "dict interface") # ignore spglib warning from v2.4.1 symm_dataset, _unique_sites = _get_symm_dataset_of_struc_with_all_equiv_sites( defect.site.frac_coords, defect.structure, symm_ops=symm_ops, symprec=symprec ) @@ -1200,7 +1199,6 @@ def point_symmetry_from_defect_entry( a boolean specifying if the supercell has been detected to break the crystal periodicity). """ - warnings.filterwarnings("ignore", "dict interface") # ignore spglib warning from v2.4.1 if symprec is None: symprec = 0.1 if relaxed else 0.01 # relaxed structures likely have structural noise # May need to adjust symprec (e.g. for Ag2Se, symprec of 0.2 is too large as we have very @@ -1506,7 +1504,6 @@ def point_symmetry( a boolean specifying if the supercell has been detected to break the crystal periodicity). """ - warnings.filterwarnings("ignore", "dict interface") # ignore spglib warning from v2.4.1 if symprec is None: symprec = 0.1 if relaxed else 0.01 # relaxed structures likely have structural noise diff --git a/pyproject.toml b/pyproject.toml index 4f8141e3..68861e74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,11 +23,6 @@ dependencies = [ "numpy>=1.21.0", "pymatgen>=2024.5.31", # limiting factor: breaking changes in `VaspInputSet`; # https://github.com/materialsproject/pymatgen/issues/3860 - "spglib>=2.4.0", # previously explicitly set to <=2.0.2 to avoid unnecessary warnings - # (https://github.com/spglib/spglib/issues/338) but causing dependency issues - # (https://github.com/SMTG-Bham/doped/issues/73), being worked on in - # https://github.com/spglib/spglib/issues/462 -- update Troubleshooting.rst and spglib warning in - # doped.utils.symmetry when finally resolved "pymatgen-analysis-defects>=2023.8.22", "shakenbreak>=3.3.4", # oxi-state functions renaming "pandas", From 8851f877f56ca6406bdaf2ba82068f27935e33d9 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Sat, 3 Aug 2024 14:06:20 -0400 Subject: [PATCH 10/17] Implement updated naming convention for competing phases (with spacegroups) --- doped/chemical_potentials.py | 58 ++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index 4d946f02..9a76a15d 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -810,6 +810,42 @@ def prune_entries_to_border_candidates( return bordering_entries +def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=False): + """ + Get the ``doped`` name for a competing phase entry from the Materials + Project (MP) database. + + The default naming convention in ``doped`` for competing phases is: + ``"{Chemical Formula}_{Space Group}_EaH_{MP Energy above Hull}"``. + This is stored in the ``entry.data["doped_name"]`` key-value pair. + If this value is already set, then this function just returns the + previously-generated ``doped`` name, unless ``regenerate=True``. + + Args: + entry (ComputedStructureEntry): + ``pymatgen`` ``ComputedStructureEntry`` object for the + competing phase. + regenerate (bool): + Whether to regenerate the ``doped`` name for the competing + phase, if ``entry.data["doped_name"]`` already set. + Default is False. + + Returns: + doped_name (str): + The ``doped`` name for the competing phase, to use as folder + name when generating calculation inputs. + """ + if not entry.data.get("doped_name") or regenerate: # not set, so generate + rounded_eah = round(_get_e_above_hull(entry.data), 4) + if np.isclose(rounded_eah, 0): + rounded_eah = 0 + entry.data["doped_name"] = ( + f"{entry.name}_{entry.structure.get_space_group_info()[0]}_EaH_{rounded_eah}" + ) + + return entry.data.get("doped_name") + + class CompetingPhases: def __init__( self, @@ -992,6 +1028,7 @@ def __init__( # sort by energy above hull, num_species, then alphabetically: self.entries.sort(key=lambda x: _entries_sorting_func(x)) + _names = [get_and_set_competing_phase_name(entry) for entry in self.entries] # set names if not self.legacy_MP: # need to pull ``SummaryDoc``s to get band_gap and magnetization info self.MP_docs = get_MP_summary_docs( @@ -1077,13 +1114,9 @@ def convergence_setup( + ("_" * (dict_set.kpoints.kpts[0][0] // 10)) + ",".join(str(k) for k in dict_set.kpoints.kpts[0]) ) - fname = f"competing_phases/{self._competing_phase_name(e)}/kpoint_converge/{kname}" + fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/kpoint_converge/{kname}" # TODO: competing_phases folder name should be an optional parameter, and rename default # to something that isn't so ugly? CompetingPhases? - # TODO: Naming should be done in __init__ to ensure consistency and efficiency. Watch - # out for cases where rounding can give same name (e.g. Te!) - should use - # {formula}_MP_{mpid}_EaH_{round(e_above_hull,4)} as naming convention, to prevent any - # rare cases of overwriting -- should use space-group in names! dict_set.write_input(fname, **kwargs) for e in self.metals: @@ -1107,15 +1140,9 @@ def convergence_setup( + ("_" * (dict_set.kpoints.kpts[0][0] // 10)) + ",".join(str(k) for k in dict_set.kpoints.kpts[0]) ) - fname = f"competing_phases/{self._competing_phase_name(e)}/kpoint_converge/{kname}" + fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/kpoint_converge/{kname}" dict_set.write_input(fname, **kwargs) - def _competing_phase_name(self, entry): - rounded_eah = round(_get_e_above_hull(entry.data), 4) - if np.isclose(rounded_eah, 0): - return f"{entry.name}_EaH_0" - return f"{entry.name}_EaH_{rounded_eah}" - # TODO: Add vasp_ncl_setup() def vasp_std_setup( self, @@ -1189,7 +1216,7 @@ def vasp_std_setup( force_gamma=True, ) - fname = f"competing_phases/{self._competing_phase_name(e)}/vasp_std" + fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/vasp_std" dict_set.write_input(fname, **kwargs) for e in self.metals: @@ -1206,7 +1233,7 @@ def vasp_std_setup( force_gamma=True, ) - fname = f"competing_phases/{self._competing_phase_name(e)}/vasp_std" + fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/vasp_std" dict_set.write_input(fname, **kwargs) for e in self.molecules: # gamma-only for molecules @@ -1230,7 +1257,7 @@ def vasp_std_setup( user_potcar_functional=user_potcar_functional, force_gamma=True, ) - fname = f"competing_phases/{self._competing_phase_name(e)}/vasp_std" + fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/vasp_std" dict_set.write_input(fname, **kwargs) def _set_spin_polarisation(self, incar_settings, user_incar_settings, entry): @@ -1539,6 +1566,7 @@ def __init__( self.MP_full_pd_entries.sort(key=lambda x: _entries_sorting_func(x)) self.MP_full_pd = PhaseDiagram(self.MP_full_pd_entries) self.entries.sort(key=lambda x: _entries_sorting_func(x)) + _names = [get_and_set_competing_phase_name(entry) for entry in self.entries] # set doped names class CompetingPhasesAnalyzer: From 1ea7af4ccf070b1d93d3f226b16092c2dc74e93a Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Sat, 3 Aug 2024 14:21:40 -0400 Subject: [PATCH 11/17] Update default `e_above_hull` as discussed --- doped/chemical_potentials.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index 9a76a15d..dbba0577 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -713,7 +713,7 @@ def prune_entries_to_border_candidates( entries: list[ComputedEntry], bulk_computed_entry: ComputedEntry, phase_diagram: Optional[PhaseDiagram] = None, - e_above_hull: float = 0.1, + e_above_hull: float = 0.05, ): """ Given an input list of ``ComputedEntry``/``ComputedStructureEntry``s @@ -722,7 +722,7 @@ def prune_entries_to_border_candidates( border the host on the phase diagram (and therefore be a competing phase which determines the host chemical potential limits), allowing for an error tolerance for the semi-local DFT database energies (``e_above_hull``, set - to ``self.e_above_hull`` 0.1 eV/atom by default). + to ``self.e_above_hull`` 0.05 eV/atom by default). If ``phase_diagram`` is provided then this is used as the reference phase diagram, otherwise it is generated from ``entries`` and @@ -749,7 +749,7 @@ def prune_entries_to_border_candidates( All phases that would border the host material on the phase diagram, if their relative energy was downshifted by ``e_above_hull``, are included. - (Default is 0.1 eV/atom). + (Default is 0.05 eV/atom). Returns: list[ComputedEntry]: @@ -850,7 +850,7 @@ class CompetingPhases: def __init__( self, composition: Union[str, Composition], - e_above_hull: float = 0.1, + e_above_hull: float = 0.05, api_key: Optional[str] = None, full_phase_diagram: bool = False, ): @@ -862,11 +862,15 @@ def __init__( For this, the Materials Project (MP) database is queried using the ``MPRester`` API, and any calculated compounds which `could` border the host material within an error tolerance for the semi-local DFT - database energies (``e_above_hull``, 0.1 eV/atom by default) are + database energies (``e_above_hull``, 0.05 eV/atom by default) are generated, along with the elemental reference phases. Diatomic gaseous molecules are generated as molecules-in-a-box as appropriate (e.g. for O2, F2, H2 etc). + Often ``e_above_hull`` can be lowered to reduce the number of + calculations while retaining good accuracy relative to the typical + error of defect calculations. + Args: composition (str, ``Composition``): Composition of the host material (e.g. ``'LiFePO4'``, or @@ -881,7 +885,10 @@ def __init__( All phases that would border the host material on the phase diagram, if their relative energy was downshifted by ``e_above_hull``, are included. - (Default is 0.1 eV/atom). + Often ``e_above_hull`` can be lowered to reduce the number of + calculations while retaining good accuracy relative to the + typical error of defect calculations. + (Default is 0.05 eV/atom). api_key (str): Materials Project (MP) API key, needed to access the MP database for competing phase generation. If not supplied, will @@ -1350,7 +1357,7 @@ def __init__( self, composition: Union[str, Composition], extrinsic_species: Union[str, Iterable], - e_above_hull: float = 0.1, + e_above_hull: float = 0.05, full_sub_approach: bool = False, codoping: bool = False, api_key: Optional[str] = None, @@ -1361,6 +1368,10 @@ def __init__( the ``e_above_hull`` error range to generate potential competing phases. + Often ``e_above_hull`` can be lowered to reduce the number of + calculations while retaining good accuracy relative to the typical + error of defect calculations. + Args: composition (str, Composition): Composition of host material (e.g. 'LiFePO4', or Composition('LiFePO4'), @@ -1378,7 +1389,12 @@ def __init__( choice (GGA vs hybrid DFT / GGA+U / RPA etc.), lack of vdW corrections etc. Any phases that would border the host material on the phase diagram, if their relative energy was downshifted by ``e_above_hull``, are included. - Default is 0.1 eV/atom. + + Often ``e_above_hull`` can be lowered to reduce the number of + calculations while retaining good accuracy relative to the typical + error of defect calculations. + + Default is 0.05 eV/atom. full_sub_approach (bool): Generate competing phases by considering the full phase diagram, including chemical potential limits with multiple extrinsic phases. Only recommended when From 12c6f24612646dbc9072fd9abb25653a463fe28a Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Sat, 3 Aug 2024 14:52:13 -0400 Subject: [PATCH 12/17] Handle potential duplicate names dynamically --- doped/chemical_potentials.py | 38 ++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index dbba0577..1c9e5a2e 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -810,7 +810,7 @@ def prune_entries_to_border_candidates( return bordering_entries -def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=False): +def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=False, ndigits=3) -> str: """ Get the ``doped`` name for a competing phase entry from the Materials Project (MP) database. @@ -829,6 +829,9 @@ def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=F Whether to regenerate the ``doped`` name for the competing phase, if ``entry.data["doped_name"]`` already set. Default is False. + ndigits (int): + Number of digits to round the energy above hull value (in + eV/atom) to. Default is 3. Returns: doped_name (str): @@ -836,7 +839,7 @@ def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=F name when generating calculation inputs. """ if not entry.data.get("doped_name") or regenerate: # not set, so generate - rounded_eah = round(_get_e_above_hull(entry.data), 4) + rounded_eah = round(_get_e_above_hull(entry.data), ndigits) if np.isclose(rounded_eah, 0): rounded_eah = 0 entry.data["doped_name"] = ( @@ -846,6 +849,33 @@ def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=F return entry.data.get("doped_name") +def _name_entries_and_handle_duplicates(entries: list[ComputedStructureEntry]): + """ + Given an input list of ``ComputedStructureEntry`` objects, sets the + ``entry.data["doped_name"]`` values using + ``get_and_set_competing_phase_name``, and increases ``ndigits`` (rounding + for energy above hull in name) dynamically from 3 -> 4 -> 5 to ensure no + duplicate names. + """ + ndigits = 3 + entry_names = [get_and_set_competing_phase_name(entry, ndigits=ndigits) for entry in entries] + while duplicate_entries := [ + entries[i] for i, name in enumerate(entry_names) if entry_names.count(name) > 1 + ]: + if ndigits == 5: + warnings.warn( + f"Duplicate entry names found for generated competing phases: " + f"{get_and_set_competing_phase_name(duplicate_entries[0])}!" + ) + break + ndigits += 1 + _duplicate_entry_names = [ + get_and_set_competing_phase_name(entry, regenerate=True, ndigits=ndigits) + for entry in duplicate_entries + ] + entry_names = [get_and_set_competing_phase_name(entry) for entry in entries] + + class CompetingPhases: def __init__( self, @@ -1035,7 +1065,7 @@ def __init__( # sort by energy above hull, num_species, then alphabetically: self.entries.sort(key=lambda x: _entries_sorting_func(x)) - _names = [get_and_set_competing_phase_name(entry) for entry in self.entries] # set names + _name_entries_and_handle_duplicates(self.entries) # set entry names if not self.legacy_MP: # need to pull ``SummaryDoc``s to get band_gap and magnetization info self.MP_docs = get_MP_summary_docs( @@ -1582,7 +1612,7 @@ def __init__( self.MP_full_pd_entries.sort(key=lambda x: _entries_sorting_func(x)) self.MP_full_pd = PhaseDiagram(self.MP_full_pd_entries) self.entries.sort(key=lambda x: _entries_sorting_func(x)) - _names = [get_and_set_competing_phase_name(entry) for entry in self.entries] # set doped names + _name_entries_and_handle_duplicates(self.entries) # set entry names class CompetingPhasesAnalyzer: From a8badc3f65fe589c97a1eb443a6a584e250e2cc6 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Sat, 3 Aug 2024 22:58:47 -0400 Subject: [PATCH 13/17] Add test for competing phase (re)naming --- doped/chemical_potentials.py | 4 +-- tests/test_chemical_potentials.py | 55 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index 1c9e5a2e..b374d561 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -862,18 +862,18 @@ def _name_entries_and_handle_duplicates(entries: list[ComputedStructureEntry]): while duplicate_entries := [ entries[i] for i, name in enumerate(entry_names) if entry_names.count(name) > 1 ]: + ndigits += 1 if ndigits == 5: warnings.warn( f"Duplicate entry names found for generated competing phases: " f"{get_and_set_competing_phase_name(duplicate_entries[0])}!" ) break - ndigits += 1 _duplicate_entry_names = [ get_and_set_competing_phase_name(entry, regenerate=True, ndigits=ndigits) for entry in duplicate_entries ] - entry_names = [get_and_set_competing_phase_name(entry) for entry in entries] + entry_names = [get_and_set_competing_phase_name(entry, regenerate=False) for entry in entries] class CompetingPhases: diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index 36d61b8b..98125c15 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -184,6 +184,61 @@ def check_O2_entry(self, cp): assert result[0].data["molecule"] assert np.isclose(result[0].data["energy_per_atom"], -4.94795546875) + @parameterized_subtest() + def test_entry_naming(self, api_key): + """ + Test the naming functions for competing phase entries in ``doped``, + including rounding to "_0" and increasing the number of digits if + duplicates are encountered. + """ + cdte_cp = chemical_potentials.CompetingPhases("CdTe", api_key=api_key) + if len(api_key) != 32: + assert [entry.data["doped_name"] for entry in cdte_cp.entries] == [ + "Cd_P6_3/mmc_EaH_0", + "Te_P3_121_EaH_0", + "CdTe_F-43m_EaH_0", + "Te_P3_221_EaH_0", + "Cd_Fm-3m_EaH_0.001", + "Cd_R-3m_EaH_0.001", + "CdTe_P6_3mc_EaH_0.003", + "CdTe_Cmc2_1_EaH_0.006", + "Cd_P6_3/mmc_EaH_0.018", + "Te_C2/m_EaH_0.044", + "Te_Pm-3m_EaH_0.047", + "Te_Pmma_EaH_0.047", + "Te_Pmc2_1_EaH_0.049", + ] + else: # slightly different for new MP API, Te entries the same + for i in [ + "Te_P3_121_EaH_0", + "Te_P3_221_EaH_0", + "Te_C2/m_EaH_0.044", + "Te_Pm-3m_EaH_0.047", + "Te_Pmma_EaH_0.047", + "Te_Pmc2_1_EaH_0.049", + ]: + assert i in [entry.data["doped_name"] for entry in cdte_cp.entries] + + # test case when the EaH rounding needs to be dynamically updated: + # (this will be quite a rare case, as it requires two phases with the same formula, space group + # and energy above hull to 1 meV/atom + from copy import deepcopy + + cds_cp = chemical_potentials.CompetingPhases("CdS", api_key=api_key) + assert "S_Pnnm_EaH_0.014" in [entry.data["doped_name"] for entry in cds_cp.entries] + new_entry = deepcopy( + next([entry for entry in cds_cp.entries if entry.data["doped_name"] == "S_Pnnm_EaH_0.014"]) + ) # duplicate entry to force renaming + if len(api_key) != 32: + new_entry.data["e_above_hull"] += 2e-4 + else: + new_entry.data["energy_above_hull"] += 2e-4 + chemical_potentials._name_entries_and_handle_duplicates([*cds_cp.entries, new_entry]) + entry_names = [entry.data["doped_name"] for entry in [*cds_cp.entries, new_entry]] + assert "S_Pnnm_EaH_0.014" not in entry_names + assert "S_Pnnm_EaH_0.0141" in entry_names + assert "S_Pnnm_EaH_0.0143" in entry_names + @parameterized_subtest() def test_unstable_host(self, api_key): """ From 0f3e5c18fced78706b96449771138f9928a75067 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Sun, 4 Aug 2024 11:06:58 -0400 Subject: [PATCH 14/17] Update to new default `CompetingPhases` folder naming and clean up tests --- doped/chemical_potentials.py | 56 ++--- .../La2O3_EaH_0.0/relax/vasprun.xml.gz | Bin .../La2Zr2O7_EaH_0.0/relax/vasprun.xml.gz | Bin .../La_ZrO2/La_EaH_0.0/relax/vasprun.xml.gz | Bin .../La_ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz | Bin .../Zr2O_EaH_0.008/relax/vasprun.xml.gz | Bin .../La_ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz | Bin .../Zr3O_EaH_0.004/relax/vasprun.xml.gz | Bin .../La_ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz | Bin .../ZrO2_EaH_0.009/relax/vasprun.xml.gz | Bin .../La_ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz | Bin .../La_ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz | Bin .../MgO_EaH_0.0/kpoint_converge/k4,4,4/INCAR | 0 .../kpoint_converge/k4,4,4/KPOINTS | 0 .../MgO_EaH_0.0/kpoint_converge/k4,4,4/POSCAR | 0 .../MgO_EaH_0.0/kpoint_converge/k5,5,5/INCAR | 0 .../kpoint_converge/k5,5,5/KPOINTS | 0 .../MgO_EaH_0.0/kpoint_converge/k5,5,5/POSCAR | 0 .../MgO_EaH_0.0/kpoint_converge/k6,6,6/INCAR | 0 .../kpoint_converge/k6,6,6/KPOINTS | 0 .../MgO_EaH_0.0/kpoint_converge/k6,6,6/POSCAR | 0 .../MgO_EaH_0.0/kpoint_converge/k7,7,7/INCAR | 0 .../kpoint_converge/k7,7,7/KPOINTS | 0 .../MgO_EaH_0.0/kpoint_converge/k7,7,7/POSCAR | 0 .../MgO_EaH_0.0/kpoint_converge/k8,8,8/INCAR | 0 .../kpoint_converge/k8,8,8/KPOINTS | 0 .../MgO_EaH_0.0/kpoint_converge/k8,8,8/POSCAR | 0 .../MgO_EaH_0.0/kpoint_converge/k9,9,9/INCAR | 0 .../kpoint_converge/k9,9,9/KPOINTS | 0 .../MgO_EaH_0.0/kpoint_converge/k9,9,9/POSCAR | 0 .../MgO/MgO_EaH_0.0/vasp_std/INCAR | 0 .../MgO/MgO_EaH_0.0/vasp_std/KPOINTS | 0 .../MgO/MgO_EaH_0.0/vasp_std/POSCAR | 0 .../MgO/MgO_EaH_0.0/vasp_std/vasprun.xml | 0 .../vasprun.xml_09_04_59on25_01_24.gz | Bin .../Mg_EaH_0.0/kpoint_converge/k5,5,5/INCAR | 0 .../Mg_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS | 0 .../Mg_EaH_0.0/kpoint_converge/k5,5,5/POSCAR | 0 .../Mg_EaH_0.0/kpoint_converge/k6,6,6/INCAR | 0 .../Mg_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS | 0 .../Mg_EaH_0.0/kpoint_converge/k6,6,6/POSCAR | 0 .../Mg_EaH_0.0/kpoint_converge/k7,7,7/INCAR | 0 .../Mg_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS | 0 .../Mg_EaH_0.0/kpoint_converge/k7,7,7/POSCAR | 0 .../MgO/Mg_EaH_0.0/vasp_std/INCAR | 0 .../MgO/Mg_EaH_0.0/vasp_std/KPOINTS | 0 .../MgO/Mg_EaH_0.0/vasp_std/POSCAR | 0 .../MgO/Mg_EaH_0.0/vasp_std/vasprun.xml | 0 .../vasprun.xml_09_03_30on25_01_24.gz | Bin .../kpoint_converge/k10,10,6/INCAR | 0 .../kpoint_converge/k10,10,6/KPOINTS | 0 .../kpoint_converge/k10,10,6/POSCAR | 0 .../kpoint_converge/k7,7,4/INCAR | 0 .../kpoint_converge/k7,7,4/KPOINTS | 0 .../kpoint_converge/k7,7,4/POSCAR | 0 .../kpoint_converge/k8,8,5/INCAR | 0 .../kpoint_converge/k8,8,5/KPOINTS | 0 .../kpoint_converge/k8,8,5/POSCAR | 0 .../kpoint_converge/k9,9,5/INCAR | 0 .../kpoint_converge/k9,9,5/KPOINTS | 0 .../kpoint_converge/k9,9,5/POSCAR | 0 .../kpoint_converge/k9,9,6/INCAR | 0 .../kpoint_converge/k9,9,6/KPOINTS | 0 .../kpoint_converge/k9,9,6/POSCAR | 0 .../MgO/Mg_EaH_0.0061/vasp_std/INCAR | 0 .../MgO/Mg_EaH_0.0061/vasp_std/KPOINTS | 0 .../MgO/Mg_EaH_0.0061/vasp_std/POSCAR | 0 .../MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml | 0 .../vasprun.xml_09_04_01on25_01_24.gz | Bin .../kpoint_converge/k10,10,3/INCAR | 0 .../kpoint_converge/k10,10,3/KPOINTS | 0 .../kpoint_converge/k10,10,3/POSCAR | 0 .../kpoint_converge/k7,7,2/INCAR | 0 .../kpoint_converge/k7,7,2/KPOINTS | 0 .../kpoint_converge/k7,7,2/POSCAR | 0 .../kpoint_converge/k8,8,2/INCAR | 0 .../kpoint_converge/k8,8,2/KPOINTS | 0 .../kpoint_converge/k8,8,2/POSCAR | 0 .../kpoint_converge/k9,9,2/INCAR | 0 .../kpoint_converge/k9,9,2/KPOINTS | 0 .../kpoint_converge/k9,9,2/POSCAR | 0 .../kpoint_converge/k9,9,3/INCAR | 0 .../kpoint_converge/k9,9,3/KPOINTS | 0 .../kpoint_converge/k9,9,3/POSCAR | 0 .../MgO/Mg_EaH_0.0099/vasp_std/INCAR | 0 .../MgO/Mg_EaH_0.0099/vasp_std/KPOINTS | 0 .../MgO/Mg_EaH_0.0099/vasp_std/POSCAR | 0 .../MgO/Mg_EaH_0.0099/vasp_std/vasprun.xml | 0 .../MgO/O2_EaH_0.0/vasp_std/INCAR | 0 .../MgO/O2_EaH_0.0/vasp_std/KPOINTS | 0 .../MgO/O2_EaH_0.0/vasp_std/POSCAR | 0 .../MgO/O2_EaH_0.0/vasp_std/vasprun.xml | 0 .../vasprun.xml_09_37_34on25_01_24.gz | Bin .../ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz | Bin .../ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz | Bin .../ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz | Bin .../ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz | Bin .../ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz | Bin .../ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz | Bin .../ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz | Bin .../ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz | Bin .../mgo_chempots.csv | 0 .../mgo_chempots.json | 0 .../mgo_competing_phase_energies.csv | 0 .../ytos_phase_diagram.json | 0 .../zro2_chempots.csv | 0 .../zro2_chempots.json | 0 .../zro2_competing_phase_energies.csv | 0 .../zro2_la_chempots.csv | 0 .../zro2_la_chempots.json | 0 .../zro2_la_competing_phase_energies.csv | 0 .../zro2_y_chempots.json | 0 tests/test_chemical_potentials.py | 205 ++++++++---------- tests/test_thermodynamics.py | 2 +- 114 files changed, 121 insertions(+), 142 deletions(-) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/La2O3_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/La2Zr2O7_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/La_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/La_ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/vasp_std/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/vasp_std/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/vasp_std/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml (100%) rename examples/{competing_phases => CompetingPhases}/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml_09_04_59on25_01_24.gz (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/vasp_std/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/vasp_std/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/vasp_std/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml_09_03_30on25_01_24.gz (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/vasp_std/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/vasp_std/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/vasp_std/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml_09_04_01on25_01_24.gz (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/vasp_std/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/vasp_std/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/vasp_std/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/Mg_EaH_0.0099/vasp_std/vasprun.xml (100%) rename examples/{competing_phases => CompetingPhases}/MgO/O2_EaH_0.0/vasp_std/INCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/O2_EaH_0.0/vasp_std/KPOINTS (100%) rename examples/{competing_phases => CompetingPhases}/MgO/O2_EaH_0.0/vasp_std/POSCAR (100%) rename examples/{competing_phases => CompetingPhases}/MgO/O2_EaH_0.0/vasp_std/vasprun.xml (100%) rename examples/{competing_phases => CompetingPhases}/MgO/O2_EaH_0.0/vasp_std/vasprun.xml_09_37_34on25_01_24.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz (100%) rename examples/{competing_phases => CompetingPhases}/mgo_chempots.csv (100%) rename examples/{competing_phases => CompetingPhases}/mgo_chempots.json (100%) rename examples/{competing_phases => CompetingPhases}/mgo_competing_phase_energies.csv (100%) rename examples/{competing_phases => CompetingPhases}/ytos_phase_diagram.json (100%) rename examples/{competing_phases => CompetingPhases}/zro2_chempots.csv (100%) rename examples/{competing_phases => CompetingPhases}/zro2_chempots.json (100%) rename examples/{competing_phases => CompetingPhases}/zro2_competing_phase_energies.csv (100%) rename examples/{competing_phases => CompetingPhases}/zro2_la_chempots.csv (100%) rename examples/{competing_phases => CompetingPhases}/zro2_la_chempots.json (100%) rename examples/{competing_phases => CompetingPhases}/zro2_la_competing_phase_energies.csv (100%) rename examples/{competing_phases => CompetingPhases}/zro2_y_chempots.json (100%) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index b374d561..b6de3df6 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -810,7 +810,9 @@ def prune_entries_to_border_candidates( return bordering_entries -def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=False, ndigits=3) -> str: +def get_and_set_competing_phase_name( + entry: Union[ComputedStructureEntry, ComputedEntry], regenerate=False, ndigits=3 +) -> str: """ Get the ``doped`` name for a competing phase entry from the Materials Project (MP) database. @@ -822,7 +824,7 @@ def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=F previously-generated ``doped`` name, unless ``regenerate=True``. Args: - entry (ComputedStructureEntry): + entry (ComputedStructureEntry, ComputedEntry): ``pymatgen`` ``ComputedStructureEntry`` object for the competing phase. regenerate (bool): @@ -842,9 +844,8 @@ def get_and_set_competing_phase_name(entry: ComputedStructureEntry, regenerate=F rounded_eah = round(_get_e_above_hull(entry.data), ndigits) if np.isclose(rounded_eah, 0): rounded_eah = 0 - entry.data["doped_name"] = ( - f"{entry.name}_{entry.structure.get_space_group_info()[0]}_EaH_{rounded_eah}" - ) + space_group = entry.structure.get_space_group_info()[0] if hasattr(entry, "structure") else "NA" + entry.data["doped_name"] = f"{entry.name}_{space_group}_EaH_{rounded_eah}" return entry.data.get("doped_name") @@ -1151,9 +1152,7 @@ def convergence_setup( + ("_" * (dict_set.kpoints.kpts[0][0] // 10)) + ",".join(str(k) for k in dict_set.kpoints.kpts[0]) ) - fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/kpoint_converge/{kname}" - # TODO: competing_phases folder name should be an optional parameter, and rename default - # to something that isn't so ugly? CompetingPhases? + fname = f"CompetingPhases/{get_and_set_competing_phase_name(e)}/kpoint_converge/{kname}" dict_set.write_input(fname, **kwargs) for e in self.metals: @@ -1177,7 +1176,7 @@ def convergence_setup( + ("_" * (dict_set.kpoints.kpts[0][0] // 10)) + ",".join(str(k) for k in dict_set.kpoints.kpts[0]) ) - fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/kpoint_converge/{kname}" + fname = f"CompetingPhases/{get_and_set_competing_phase_name(e)}/kpoint_converge/{kname}" dict_set.write_input(fname, **kwargs) # TODO: Add vasp_ncl_setup() @@ -1253,7 +1252,7 @@ def vasp_std_setup( force_gamma=True, ) - fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/vasp_std" + fname = f"CompetingPhases/{get_and_set_competing_phase_name(e)}/vasp_std" dict_set.write_input(fname, **kwargs) for e in self.metals: @@ -1270,7 +1269,7 @@ def vasp_std_setup( force_gamma=True, ) - fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/vasp_std" + fname = f"CompetingPhases/{get_and_set_competing_phase_name(e)}/vasp_std" dict_set.write_input(fname, **kwargs) for e in self.molecules: # gamma-only for molecules @@ -1294,7 +1293,7 @@ def vasp_std_setup( user_potcar_functional=user_potcar_functional, force_gamma=True, ) - fname = f"competing_phases/{get_and_set_competing_phase_name(e)}/vasp_std" + fname = f"CompetingPhases/{get_and_set_competing_phase_name(e)}/vasp_std" dict_set.write_input(fname, **kwargs) def _set_spin_polarisation(self, incar_settings, user_incar_settings, entry): @@ -1373,16 +1372,7 @@ def _generate_elemental_diatomic_phases(self, entries: list[ComputedEntry]): return formatted_entries -# TODO: This doesn't need to be a whole extra class right? Better just amalgamated? class ExtrinsicCompetingPhases(CompetingPhases): - """ - This class generates the competing phases that need to be calculated to - obtain the chemical potential limits when doping with extrinsic species / - impurities. - - Ensures that only the necessary additional competing phases are generated. - """ - def __init__( self, composition: Union[str, Composition], @@ -1394,9 +1384,22 @@ def __init__( full_phase_diagram: bool = False, ): """ - This code uses the Materials Project (MP) phase diagram data along with - the ``e_above_hull`` error range to generate potential competing - phases. + Class to generate VASP input files for competing phases involving + extrinsic (dopant/impurity) elements, which determine the chemical + potential limits for those elements in the host compound. + + Only extrinsic competing phases are contained in the + ``ExtrinsicCompetingPhases.entries`` list (used for input file generation), + while the `intrinsic` competing phases for the host compound are stored in + ``ExtrinsicCompetingPhases.intrinsic_entries``. + + For this, the Materials Project (MP) database is queried using the + ``MPRester`` API, and any calculated compounds which `could` border + the host material within an error tolerance for the semi-local DFT + database energies (``e_above_hull``, 0.05 eV/atom by default) are + generated, along with the elemental reference phases. + Diatomic gaseous molecules are generated as molecules-in-a-box as + appropriate (e.g. for O2, F2, H2 etc). Often ``e_above_hull`` can be lowered to reduce the number of calculations while retaining good accuracy relative to the typical @@ -1455,8 +1458,7 @@ def __init__( downshifted by ``e_above_hull`` eV/atom. (Default is ``False``). """ - # competing phases & entries of the OG system: - super().__init__( + super().__init__( # competing phases & entries of the OG system: composition=composition, e_above_hull=e_above_hull, api_key=api_key, @@ -1651,7 +1653,7 @@ def __init__(self, composition: Union[str, Composition]): # which auto-parses vaspruns from the subdirectories (or optionally a list of vaspruns, # or a csv path); see shelved changes for this # TODO: Could add multiprocessing like DefectsParser to expedite parsing? - def from_vaspruns(self, path="competing_phases", folder="vasp_std", csv_path=None, verbose=True): + def from_vaspruns(self, path="CompetingPhases", folder="vasp_std", csv_path=None, verbose=True): """ Parses competing phase energies from ``vasprun.xml(.gz)`` outputs, computes the formation energies and generates the diff --git a/examples/competing_phases/La_ZrO2/La2O3_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/La2O3_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/La2O3_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/La2O3_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/La2Zr2O7_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/La2Zr2O7_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/La2Zr2O7_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/La2Zr2O7_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/La_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/La_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/La_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/La_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/La_ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz b/examples/CompetingPhases/La_ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/La_ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz rename to examples/CompetingPhases/La_ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k4,4,4/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k5,5,5/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k6,6,6/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k7,7,7/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k8,8,8/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/kpoint_converge/k9,9,9/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/INCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/INCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/INCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/INCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/KPOINTS b/examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/KPOINTS rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/KPOINTS diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/POSCAR b/examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/POSCAR similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/POSCAR rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/POSCAR diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml b/examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml diff --git a/examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml_09_04_59on25_01_24.gz b/examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml_09_04_59on25_01_24.gz similarity index 100% rename from examples/competing_phases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml_09_04_59on25_01_24.gz rename to examples/CompetingPhases/MgO/MgO_EaH_0.0/vasp_std/vasprun.xml_09_04_59on25_01_24.gz diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k5,5,5/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k6,6,6/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/kpoint_converge/k7,7,7/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml b/examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml_09_03_30on25_01_24.gz b/examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml_09_03_30on25_01_24.gz similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml_09_03_30on25_01_24.gz rename to examples/CompetingPhases/MgO/Mg_EaH_0.0/vasp_std/vasprun.xml_09_03_30on25_01_24.gz diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k10,10,6/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k7,7,4/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k8,8,5/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,5/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/kpoint_converge/k9,9,6/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml_09_04_01on25_01_24.gz b/examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml_09_04_01on25_01_24.gz similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml_09_04_01on25_01_24.gz rename to examples/CompetingPhases/MgO/Mg_EaH_0.0061/vasp_std/vasprun.xml_09_04_01on25_01_24.gz diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k10,10,3/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k7,7,2/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k8,8,2/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,2/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/kpoint_converge/k9,9,3/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/INCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/INCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/INCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/INCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/KPOINTS b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/KPOINTS rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/KPOINTS diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/POSCAR b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/POSCAR similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/POSCAR rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/POSCAR diff --git a/examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/vasprun.xml b/examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/vasprun.xml similarity index 100% rename from examples/competing_phases/MgO/Mg_EaH_0.0099/vasp_std/vasprun.xml rename to examples/CompetingPhases/MgO/Mg_EaH_0.0099/vasp_std/vasprun.xml diff --git a/examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/INCAR b/examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/INCAR similarity index 100% rename from examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/INCAR rename to examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/INCAR diff --git a/examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/KPOINTS b/examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/KPOINTS similarity index 100% rename from examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/KPOINTS rename to examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/KPOINTS diff --git a/examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/POSCAR b/examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/POSCAR similarity index 100% rename from examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/POSCAR rename to examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/POSCAR diff --git a/examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml b/examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml similarity index 100% rename from examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml rename to examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml diff --git a/examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml_09_37_34on25_01_24.gz b/examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml_09_37_34on25_01_24.gz similarity index 100% rename from examples/competing_phases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml_09_37_34on25_01_24.gz rename to examples/CompetingPhases/MgO/O2_EaH_0.0/vasp_std/vasprun.xml_09_37_34on25_01_24.gz diff --git a/examples/competing_phases/ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/O2_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/Zr2O_EaH_0.008/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/Zr3O_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/Zr3O_EaH_0.004/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/ZrO2_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/ZrO2_EaH_0.009/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/Zr_EaH_0.0/relax/vasprun.xml.gz diff --git a/examples/competing_phases/ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz b/examples/CompetingPhases/ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz similarity index 100% rename from examples/competing_phases/ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz rename to examples/CompetingPhases/ZrO2/Zr_EaH_0.016/relax/vasprun.xml.gz diff --git a/examples/competing_phases/mgo_chempots.csv b/examples/CompetingPhases/mgo_chempots.csv similarity index 100% rename from examples/competing_phases/mgo_chempots.csv rename to examples/CompetingPhases/mgo_chempots.csv diff --git a/examples/competing_phases/mgo_chempots.json b/examples/CompetingPhases/mgo_chempots.json similarity index 100% rename from examples/competing_phases/mgo_chempots.json rename to examples/CompetingPhases/mgo_chempots.json diff --git a/examples/competing_phases/mgo_competing_phase_energies.csv b/examples/CompetingPhases/mgo_competing_phase_energies.csv similarity index 100% rename from examples/competing_phases/mgo_competing_phase_energies.csv rename to examples/CompetingPhases/mgo_competing_phase_energies.csv diff --git a/examples/competing_phases/ytos_phase_diagram.json b/examples/CompetingPhases/ytos_phase_diagram.json similarity index 100% rename from examples/competing_phases/ytos_phase_diagram.json rename to examples/CompetingPhases/ytos_phase_diagram.json diff --git a/examples/competing_phases/zro2_chempots.csv b/examples/CompetingPhases/zro2_chempots.csv similarity index 100% rename from examples/competing_phases/zro2_chempots.csv rename to examples/CompetingPhases/zro2_chempots.csv diff --git a/examples/competing_phases/zro2_chempots.json b/examples/CompetingPhases/zro2_chempots.json similarity index 100% rename from examples/competing_phases/zro2_chempots.json rename to examples/CompetingPhases/zro2_chempots.json diff --git a/examples/competing_phases/zro2_competing_phase_energies.csv b/examples/CompetingPhases/zro2_competing_phase_energies.csv similarity index 100% rename from examples/competing_phases/zro2_competing_phase_energies.csv rename to examples/CompetingPhases/zro2_competing_phase_energies.csv diff --git a/examples/competing_phases/zro2_la_chempots.csv b/examples/CompetingPhases/zro2_la_chempots.csv similarity index 100% rename from examples/competing_phases/zro2_la_chempots.csv rename to examples/CompetingPhases/zro2_la_chempots.csv diff --git a/examples/competing_phases/zro2_la_chempots.json b/examples/CompetingPhases/zro2_la_chempots.json similarity index 100% rename from examples/competing_phases/zro2_la_chempots.json rename to examples/CompetingPhases/zro2_la_chempots.json diff --git a/examples/competing_phases/zro2_la_competing_phase_energies.csv b/examples/CompetingPhases/zro2_la_competing_phase_energies.csv similarity index 100% rename from examples/competing_phases/zro2_la_competing_phase_energies.csv rename to examples/CompetingPhases/zro2_la_competing_phase_energies.csv diff --git a/examples/competing_phases/zro2_y_chempots.json b/examples/CompetingPhases/zro2_y_chempots.json similarity index 100% rename from examples/competing_phases/zro2_y_chempots.json rename to examples/CompetingPhases/zro2_y_chempots.json diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index 98125c15..316eb5e8 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -3,13 +3,12 @@ """ import os -import shutil import sys import unittest import warnings +from copy import deepcopy from functools import wraps from io import StringIO -from pathlib import Path import numpy as np import pandas as pd @@ -52,13 +51,11 @@ def wrapper(self, *args, **kwargs): class CompetingPhasesTestCase(unittest.TestCase): def setUp(self) -> None: - self.path = Path(__file__).parents[0] self.legacy_api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email (GitHub) A/C self.api_key = "UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv" # SK MP Imperial email (GitHub) A/C def tearDown(self) -> None: - if Path("competing_phases").is_dir(): - shutil.rmtree("competing_phases") + if_present_rm("CompetingPhases") @parameterized_subtest() def test_init(self, api_key): @@ -113,7 +110,7 @@ def test_init_ZnSe(self, api_key): """ cp = chemical_potentials.CompetingPhases("ZnSe", api_key=api_key) assert any(e.name == "ZnSe2" for e in cp.entries) - assert len(cp.entries) in {14, 16} # ZnSe2 present; 2 new Zn entries (mp-264...) with new MP API + assert len(cp.entries) in {11, 12} # ZnSe2 present; 2 new Zn entries (mp-264...) with new MP API znse2_entry = next(e for e in cp.entries if e.name == "ZnSe2") assert znse2_entry.data.get(cp.property_key_dict["energy_above_hull"]) == 0 assert not znse2_entry.data["molecule"] @@ -222,12 +219,10 @@ def test_entry_naming(self, api_key): # test case when the EaH rounding needs to be dynamically updated: # (this will be quite a rare case, as it requires two phases with the same formula, space group # and energy above hull to 1 meV/atom - from copy import deepcopy - cds_cp = chemical_potentials.CompetingPhases("CdS", api_key=api_key) assert "S_Pnnm_EaH_0.014" in [entry.data["doped_name"] for entry in cds_cp.entries] new_entry = deepcopy( - next([entry for entry in cds_cp.entries if entry.data["doped_name"] == "S_Pnnm_EaH_0.014"]) + next(entry for entry in cds_cp.entries if entry.data["doped_name"] == "S_Pnnm_EaH_0.014") ) # duplicate entry to force renaming if len(api_key) != 32: new_entry.data["e_above_hull"] += 2e-4 @@ -295,11 +290,16 @@ def test_unknown_host(self): w[-1].message ) if cp_settings.get("full_phase_diagram"): - assert len(cp.entries) == 45 + assert len(cp.entries) == 29 elif cp_settings.get("e_above_hull") == 0.0: assert len(cp.entries) == 8 else: - assert len(cp.entries) == 38 + assert len(cp.entries) == 26 + + # check naming of fake entry + assert "Cu2SiSe4_NA_EaH_0" in [entry.data["doped_name"] for entry in cp.entries] + + # TODO: Test file generation functions for an unknown host! @parameterized_subtest() def test_convergence_setup(self, api_key): @@ -310,20 +310,20 @@ def test_convergence_setup(self, api_key): assert cp.metals[0].data["band_gap"] == 0 assert not cp.nonmetals[0].data["molecule"] # this shouldn't exist - don't need to convergence test for molecules - assert not Path("competing_phases/O2_EaH_0.0").is_dir() + assert not os.path.exists("CompetingPhases/O2_Pmmm_EaH_0") # test if it writes out the files correctly - path1 = "competing_phases/ZrO2_EaH_0.0088/kpoint_converge/k2,1,1/" - assert Path(path1).is_dir() - with open(f"{path1}/KPOINTS", encoding="utf-8") as file: + Zro2_EaH_0pt009_folder = "CompetingPhases/ZrO2_Pbca_EaH_0.009/kpoint_converge/k2,1,1/" + assert os.path.exists(Zro2_EaH_0pt009_folder) + with open(f"{Zro2_EaH_0pt009_folder}/KPOINTS", encoding="utf-8") as file: contents = file.readlines() assert contents[3] == "2 1 1\n" - with open(f"{path1}/POTCAR.spec", encoding="utf-8") as file: + with open(f"{Zro2_EaH_0pt009_folder}/POTCAR.spec", encoding="utf-8") as file: contents = file.readlines() assert contents[0] == "Zr_sv\n" - with open(f"{path1}/INCAR", encoding="utf-8") as file: + with open(f"{Zro2_EaH_0pt009_folder}/INCAR", encoding="utf-8") as file: contents = file.readlines() assert any(line == "GGA = Ps\n" for line in contents) assert any(line == "NSW = 0\n" for line in contents) @@ -340,28 +340,28 @@ def test_vasp_std_setup(self, api_key): assert cp.molecules[0].data["molecule"] assert not cp.nonmetals[0].data["molecule"] - path1 = "competing_phases/ZrO2_EaH_0/vasp_std/" - assert Path(path1).is_dir() - with open(f"{path1}/KPOINTS", encoding="utf-8") as file: + ZrO2_EaH_0_std_folder = "CompetingPhases/ZrO2_P2_1/c_EaH_0/vasp_std/" + assert os.path.exists(ZrO2_EaH_0_std_folder) + with open(f"{ZrO2_EaH_0_std_folder}/KPOINTS", encoding="utf-8") as file: contents = file.readlines() assert "KPOINTS from doped, with reciprocal_density = 64/Å" in contents[0] assert contents[3] == "4 4 4\n" - with open(f"{path1}/POTCAR.spec", encoding="utf-8") as file: + with open(f"{ZrO2_EaH_0_std_folder}/POTCAR.spec", encoding="utf-8") as file: contents = file.readlines() assert contents == ["Zr_sv\n", "O"] - with open(f"{path1}/INCAR", encoding="utf-8") as file: + with open(f"{ZrO2_EaH_0_std_folder}/INCAR", encoding="utf-8") as file: contents = file.readlines() assert all(x in contents for x in ["AEXX = 0.25\n", "ISIF = 3\n", "GGA = Pe\n"]) - path2 = "competing_phases/O2_EaH_0/vasp_std" - assert Path(path2).is_dir() - with open(f"{path2}/KPOINTS", encoding="utf-8") as file: + O2_EaH_0_std_folder = "CompetingPhases/O2_Pmmm_EaH_0/vasp_std" + assert os.path.exists(O2_EaH_0_std_folder) + with open(f"{O2_EaH_0_std_folder}/KPOINTS", encoding="utf-8") as file: contents = file.readlines() assert contents[3] == "1 1 1\n" - struct = Structure.from_file(f"{path2}/POSCAR") + struct = Structure.from_file(f"{O2_EaH_0_std_folder}/POSCAR") assert np.isclose(struct.sites[0].frac_coords, [0.49983339, 0.5, 0.50016672]).all() assert np.isclose(struct.sites[1].frac_coords, [0.49983339, 0.5, 0.5405135]).all() @@ -411,17 +411,13 @@ def test_legacy_API_message(self): ) in output -class ExtrinsicCompetingPhasesTestCase(unittest.TestCase): - # TODO: Merge with CompetingPhases tests once actual classes merged +class ExtrinsicCompetingPhasesTestCase(unittest.TestCase): # same setUp and tearDown as above # TODO: Need to add tests for co-doping, full_sub_approach, full_phase_diagram etc!! - def setUp(self) -> None: - self.path = Path(__file__).parents[0] - self.legacy_api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email (GitHub) A/C - self.api_key = "UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv" # SK MP Imperial email (GitHub) A/C + def setUp(self): + CompetingPhasesTestCase.setUp(self) - def tearDown(self) -> None: - if Path("competing_phases").is_dir(): - shutil.rmtree("competing_phases") + def tearDown(self): + CompetingPhasesTestCase.tearDown(self) @parameterized_subtest() def test_init(self, api_key): @@ -443,40 +439,36 @@ def test_init(self, api_key): cp = chemical_potentials.ExtrinsicCompetingPhases( "ZrO2", extrinsic_species="La", api_key=api_key - ) # default e_above_hull=0.1 - assert len(cp.entries) == 5 + ) # default e_above_hull=0.05 + assert len(cp.entries) == 3 assert cp.entries[2].name == "La" # definite ordering, same 1st & 2nd as before - assert cp.entries[3].name == "LaZr9O20" # definite ordering - assert cp.entries[4].name == "LaZr9O20" # definite ordering assert all(entry.data["e_above_hull"] == 0 for entry in cp.entries[:2]) assert all(entry.data["e_above_hull"] != 0 for entry in cp.entries[2:]) - assert len(cp.intrinsic_entries) == 28 + assert len(cp.intrinsic_entries) == 18 class ChemPotAnalyzerTestCase(unittest.TestCase): def setUp(self): - self.path = Path(__file__).parents[1].joinpath("examples/competing_phases") + self.examples_path = "../examples/CompetingPhases" self.stable_system = "ZrO2" self.unstable_system = "Zr2O" self.extrinsic_species = "La" - self.csv_path = self.path / "zro2_competing_phase_energies.csv" - self.csv_path_ext = self.path / "zro2_la_competing_phase_energies.csv" - self.parsed_chempots = loadfn(self.path / "zro2_chempots.json") - self.parsed_ext_chempots = loadfn(self.path / "zro2_la_chempots.json") + self.csv_path = f"{self.examples_path}/zro2_competing_phase_energies.csv" + self.csv_path_ext = f"{self.examples_path}/zro2_la_competing_phase_energies.csv" + self.parsed_chempots = loadfn(f"{self.examples_path}/zro2_chempots.json") + self.parsed_ext_chempots = loadfn(f"{self.examples_path}/zro2_la_chempots.json") + self.parsed_ext_y_chempots = loadfn(f"{self.examples_path}/zro2_la_chempots.json") - def tearDown(self) -> None: - if Path("chempot_limits.csv").is_file(): - os.remove("chempot_limits.csv") + self.zro2_path = os.path.join(self.examples_path, "ZrO2") + self.la_zro2_path = os.path.join(self.examples_path, "La_ZrO2") - if Path("competing_phases.csv").is_file(): - os.remove("competing_phases.csv") + def tearDown(self): + for i in ["chempot_limits.csv", "CompetingPhases.csv", "input.dat"]: + if_present_rm(i) - if Path("input.dat").is_file(): - os.remove("input.dat") - - for i in os.listdir(self.path / "ZrO2"): + for i in os.listdir(os.path.join(self.examples_path, "ZrO2")): if i.startswith("."): - if_present_rm(self.path / "ZrO2" / i) + if_present_rm(os.path.join(self.examples_path, "ZrO2", i)) def test_cpa_csv(self): stable_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) @@ -496,7 +488,6 @@ def test_cpa_csv(self): -119.619571095, ) - # test chempots def test_cpa_chempots(self): stable_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) stable_cpa.from_csv(self.csv_path) @@ -574,11 +565,9 @@ def test_sort_by(self): with pytest.raises(KeyError): stable_cpa.calculate_chempots(sort_by="M") - # test vaspruns def test_vaspruns(self): cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - path = self.path / "ZrO2" - cpa.from_vaspruns(path=path, folder="relax", csv_path=self.csv_path) + cpa.from_vaspruns(path=self.zro2_path, folder="relax", csv_path=self.csv_path) assert len(cpa.elements) == 2 assert cpa.data[0]["Formula"] == "O2" @@ -590,8 +579,7 @@ def test_vaspruns(self): cpa_no.from_vaspruns(path=0) ext_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - path2 = self.path / "La_ZrO2" - ext_cpa.from_vaspruns(path=path2, folder="relax", csv_path=self.csv_path_ext) + ext_cpa.from_vaspruns(path=self.la_zro2_path, folder="relax", csv_path=self.csv_path_ext) assert len(ext_cpa.elements) == 3 assert len(ext_cpa.extrinsic_elements) == 1 # sorted by num_species, then alphabetically, then by num_atoms_in_fu, then by @@ -651,48 +639,45 @@ def test_vaspruns(self): # check if it works from a list all_paths = [] - for p in path.iterdir(): - if not p.name.startswith("."): - pp = p / "relax" / "vasprun.xml" - ppgz = p / "relax" / "vasprun.xml.gz" - if pp.is_file(): - all_paths.append(pp) - elif ppgz.is_file(): - all_paths.append(ppgz) + for subfolder in os.listdir(self.zro2_path): + if os.path.isdir(os.path.join(self.zro2_path, subfolder)) and "relax" in os.listdir( + os.path.join(self.zro2_path, subfolder) + ): + all_paths.extend( + os.path.join(self.zro2_path, subfolder, "relax", vr_file) + for vr_file in os.listdir(os.path.join(self.zro2_path, subfolder, "relax")) + if vr_file.startswith("vasprun.xml") + ) lst_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) lst_cpa.from_vaspruns(path=all_paths) assert len(lst_cpa.elements) == 2 assert len(lst_cpa.vasprun_paths) == 8 - all_fols = [p / "relax" for p in path.iterdir() if not p.name.startswith(".")] + all_folders = [path.split("/relax")[0] for path in all_paths] lst_fols_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - lst_fols_cpa.from_vaspruns(path=all_fols) + lst_fols_cpa.from_vaspruns(path=all_folders) assert len(lst_fols_cpa.elements) == 2 def test_vaspruns_hidden_files(self): cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - path = self.path / "ZrO2" - - with open(f"{path}/._OUTCAR", "w") as f: + with open(f"{self.zro2_path}/._OUTCAR", "w") as f: f.write("test pop") - with open(f"{path}/._vasprun.xml", "w") as f: + with open(f"{self.zro2_path}/._vasprun.xml", "w") as f: f.write("test pop") - with open(f"{path}/._LOCPOT", "w") as f: + with open(f"{self.zro2_path}/._LOCPOT", "w") as f: f.write("test pop") - with open(f"{path}/.DS_Store", "w") as f: + with open(f"{self.zro2_path}/.DS_Store", "w") as f: f.write("test pop") with warnings.catch_warnings(record=True) as w: - cpa.from_vaspruns(path=path, folder="relax", csv_path=self.csv_path) + cpa.from_vaspruns(path=self.zro2_path, folder="relax", csv_path=self.csv_path) print([str(warning.message) for warning in w]) # for debugging assert not w def test_vaspruns_none_parsed(self): cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - path = Path(__file__).parents[1].joinpath("examples/competing_phases") - with warnings.catch_warnings(record=True) as w, pytest.raises(FileNotFoundError) as e: - cpa.from_vaspruns(path=path, folder="relax", csv_path=self.csv_path) + cpa.from_vaspruns(path=self.examples_path, folder="relax", csv_path=self.csv_path) print([str(warning.message) for warning in w]) # for debugging assert "vasprun.xml files could not be found in the following directories (in" in str(w[0].message) assert "ZrO2 or ZrO2/relax" in str(w[0].message) @@ -704,7 +689,7 @@ def test_cplap_input(self): cpa.from_csv(self.csv_path) cpa._cplap_input(dependent_variable="O") - assert Path("input.dat").is_file() + assert os.path.exists("input.dat") with open("input.dat", encoding="utf-8") as file: contents = file.readlines() @@ -729,8 +714,7 @@ def test_cplap_input(self): def test_latex_table(self): cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - path = self.path / "ZrO2" - cpa.from_vaspruns(path=path, folder="relax", csv_path=self.csv_path) + cpa.from_vaspruns(path=self.zro2_path, folder="relax", csv_path=self.csv_path) string = cpa.to_LaTeX_table(splits=1) assert ( @@ -783,9 +767,9 @@ def test_to_csv(self): stable_cpa.from_csv(self.csv_path) stable_cpa_data = stable_cpa._get_and_sort_formation_energy_data() - stable_cpa.to_csv("competing_phases.csv") + stable_cpa.to_csv("CompetingPhases.csv") reloaded_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - reloaded_cpa.from_csv("competing_phases.csv") + reloaded_cpa.from_csv("CompetingPhases.csv") reloaded_cpa_data = reloaded_cpa._get_and_sort_formation_energy_data() print( pd.DataFrame(stable_cpa_data).to_dict(), pd.DataFrame(reloaded_cpa_data).to_dict() @@ -797,9 +781,9 @@ def test_to_csv(self): self.ext_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) self.ext_cpa.from_csv(self.csv_path_ext) - self.ext_cpa.to_csv("competing_phases.csv") + self.ext_cpa.to_csv("CompetingPhases.csv") reloaded_ext_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - reloaded_ext_cpa.from_csv("competing_phases.csv") + reloaded_ext_cpa.from_csv("CompetingPhases.csv") reloaded_ext_cpa._get_and_sort_formation_energy_data() assert reloaded_ext_cpa.formation_energy_df.round(4).equals( @@ -807,16 +791,16 @@ def test_to_csv(self): ) # test pruning: - self.ext_cpa.to_csv("competing_phases.csv", prune_polymorphs=True) - reloaded_ext_cpa.from_csv("competing_phases.csv") + self.ext_cpa.to_csv("CompetingPhases.csv", prune_polymorphs=True) + reloaded_ext_cpa.from_csv("CompetingPhases.csv") assert len(reloaded_ext_cpa.data) == 8 # polymorphs pruned assert len(self.ext_cpa.data) == 11 formulas = [i["Formula"] for i in reloaded_ext_cpa.data] assert len(formulas) == len(set(formulas)) # no polymorphs - reloaded_cpa.to_csv("competing_phases.csv", prune_polymorphs=True) - reloaded_cpa.from_csv("competing_phases.csv") + reloaded_cpa.to_csv("CompetingPhases.csv", prune_polymorphs=True) + reloaded_cpa.from_csv("CompetingPhases.csv") # check chem limits the same: _compare_chempot_dicts(reloaded_cpa.chempots, stable_cpa.chempots) @@ -834,9 +818,9 @@ def test_to_csv(self): assert self.ext_cpa.chempots["elemental_refs"] == self.parsed_ext_chempots["elemental_refs"] # test correct sorting: - self.ext_cpa.to_csv("competing_phases.csv", prune_polymorphs=True, sort_by_energy=True) + self.ext_cpa.to_csv("CompetingPhases.csv", prune_polymorphs=True, sort_by_energy=True) reloaded_ext_cpa_energy_sorted = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - reloaded_ext_cpa_energy_sorted.from_csv("competing_phases.csv") + reloaded_ext_cpa_energy_sorted.from_csv("CompetingPhases.csv") assert len(reloaded_ext_cpa.data) == 8 # polymorphs pruned assert reloaded_ext_cpa_energy_sorted.data != reloaded_ext_cpa.data # different order @@ -854,18 +838,17 @@ def test_from_csv_minimal(self): """ self.tearDown() # clear out previous csvs cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - path = self.path / "ZrO2" - cpa.from_vaspruns(path=path, folder="relax") + cpa.from_vaspruns(path=self.zro2_path, folder="relax") formation_energy_data = cpa._get_and_sort_formation_energy_data() formation_energy_df = pd.DataFrame(formation_energy_data) # drop all but the formula and energy_per_fu columns: for i in ["DFT Energy (eV/fu)", "DFT Energy (eV/atom)"]: minimal_formation_energy_df = formation_energy_df[["Formula", i]] - minimal_formation_energy_df.to_csv("competing_phases.csv", index=False) + minimal_formation_energy_df.to_csv("CompetingPhases.csv", index=False) reloaded_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) - reloaded_cpa.from_csv("competing_phases.csv") + reloaded_cpa.from_csv("CompetingPhases.csv") assert not cpa.formation_energy_df.round(4).equals( reloaded_cpa.formation_energy_df.round(4) ) # no kpoints or raw energy, but should have formula, energy_per_fu, energy_per_atom, @@ -895,10 +878,10 @@ def test_from_csv_minimal(self): # test ValueError without energy_per_fu/energy_per_atom column: too_minimal_formation_energy_df = formation_energy_df[["Formula"]] - too_minimal_formation_energy_df.to_csv("competing_phases.csv", index=False) + too_minimal_formation_energy_df.to_csv("CompetingPhases.csv", index=False) reloaded_cpa = chemical_potentials.CompetingPhasesAnalyzer(self.stable_system) with pytest.raises(ValueError) as exc: - reloaded_cpa.from_csv("competing_phases.csv") + reloaded_cpa.from_csv("CompetingPhases.csv") assert "Supplied csv does not contain the minimal columns required" in str(exc.value) def test_elements(self): @@ -958,18 +941,10 @@ def test_fe(self): # lowest energy ZrO2: assert np.isclose(formation_energy_df["Formation Energy (eV/fu)"][4], -10.975428440000002) - -class CombineExtrinsicTestCase(unittest.TestCase): - def setUp(self) -> None: - self.path = Path(__file__).parents[1].joinpath("examples/competing_phases") - first = self.path / "zro2_la_chempots.json" - second = self.path / "zro2_y_chempots.json" - self.first = loadfn(first) - self.second = loadfn(second) - self.extrinsic_species = "Y" - def test_combine_extrinsic(self): - d = chemical_potentials.combine_extrinsic(self.first, self.second, self.extrinsic_species) + d = chemical_potentials.combine_extrinsic( + self.parsed_ext_chempots, self.parsed_ext_y_chempots, "Y" + ) assert len(d["elemental_refs"].keys()) == 4 limits = list(d["limits"].keys()) assert limits[0].rsplit("-", 1)[1] == "Y2Zr2O7" @@ -977,10 +952,12 @@ def test_combine_extrinsic(self): def test_combine_extrinsic_errors(self): d = {"a": 1} with pytest.raises(KeyError): - chemical_potentials.combine_extrinsic(d, self.second, self.extrinsic_species) + chemical_potentials.combine_extrinsic(d, self.parsed_ext_y_chempots, "Y") with pytest.raises(KeyError): - chemical_potentials.combine_extrinsic(self.first, d, self.extrinsic_species) + chemical_potentials.combine_extrinsic(self.parsed_ext_chempots, d, "Y") with pytest.raises(ValueError): - chemical_potentials.combine_extrinsic(self.first, self.second, "R") + chemical_potentials.combine_extrinsic( + self.parsed_ext_chempots, self.parsed_ext_y_chempots, "R" + ) diff --git a/tests/test_thermodynamics.py b/tests/test_thermodynamics.py index 9ca62d2d..57ee8b49 100644 --- a/tests/test_thermodynamics.py +++ b/tests/test_thermodynamics.py @@ -171,7 +171,7 @@ def setUpClass(cls): cls.orig_MgO_defect_thermo = loadfn(os.path.join(cls.MgO_EXAMPLE_DIR, "MgO_thermo.json.gz")) cls.orig_MgO_defect_dict = loadfn(os.path.join(cls.MgO_EXAMPLE_DIR, "MgO_defect_dict.json.gz")) - cls.MgO_chempots = loadfn(os.path.join(cls.EXAMPLE_DIR, "competing_phases/mgo_chempots.json")) + cls.MgO_chempots = loadfn(os.path.join(cls.EXAMPLE_DIR, "CompetingPhases/mgo_chempots.json")) cls.Sb2O5_chempots = loadfn(os.path.join(data_dir, "Sb2O5/Sb2O5_chempots.json")) cls.orig_Sb2O5_defect_thermo = loadfn(os.path.join(data_dir, "Sb2O5/Sb2O5_thermo.json.gz")) From 6e59839147d75f8a319ff56c6c8c776abf39914f Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Mon, 5 Aug 2024 10:59:08 -0400 Subject: [PATCH 15/17] Allow structures as input to `CompetingPhases`, add tests --- doped/chemical_potentials.py | 182 ++++++++++++++++++++---------- doped/utils/symmetry.py | 35 +++++- tests/data/Cu2SiSe3_MP_POSCAR | 32 ++++++ tests/data/Na2FePO4F_MP_POSCAR | 80 +++++++++++++ tests/test_chemical_potentials.py | 89 +++++++++++++++ 5 files changed, 353 insertions(+), 65 deletions(-) create mode 100644 tests/data/Cu2SiSe3_MP_POSCAR create mode 100644 tests/data/Na2FePO4F_MP_POSCAR diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index b6de3df6..2bb1131a 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -18,6 +18,7 @@ import pandas as pd from monty.serialization import loadfn from pymatgen.analysis.phase_diagram import PDEntry, PhaseDiagram +from pymatgen.analysis.structure_matcher import StructureMatcher from pymatgen.core import SETTINGS, Composition, Element, Structure from pymatgen.entries.computed_entries import ( ComputedEntry, @@ -33,6 +34,7 @@ from doped import _ignore_pmg_warnings from doped.utils.parsing import _get_output_files_and_check_if_multiple, get_vasprun +from doped.utils.symmetry import get_primitive_structure from doped.vasp import MODULE_DIR, DopedDictSet, default_HSE_set, default_relax_set # globally ignore: @@ -880,7 +882,7 @@ def _name_entries_and_handle_duplicates(entries: list[ComputedStructureEntry]): class CompetingPhases: def __init__( self, - composition: Union[str, Composition], + composition: Union[str, Composition, Structure], e_above_hull: float = 0.05, api_key: Optional[str] = None, full_phase_diagram: bool = False, @@ -903,10 +905,14 @@ def __init__( error of defect calculations. Args: - composition (str, ``Composition``): + composition (str, ``Composition``, ``Structure``): Composition of the host material (e.g. ``'LiFePO4'``, or ``Composition('LiFePO4')``, or ``Composition({"Li":1, "Fe":1, "P":1, "O":4})``). + Alternatively a ``pymatgen`` ``Structure`` object for the + host material can be supplied (recommended), in which case + the primitive structure will be used as the only host + composition phase, reducing the number of calculations. e_above_hull (float): Maximum energy above hull (in eV/atom) of Materials Project entries to be considered as competing phases. This is an @@ -972,11 +978,20 @@ def __init__( # - Could have two optional EaH tolerances, a tight one (0.02 eV/atom?) that applies to all, # and a looser one (0.1 eV/atom?) that applies to phases with ICSD IDs? - self.bulk_composition = Composition(composition) - self.chemsys = list(self.bulk_composition.as_dict().keys()) + if isinstance(composition, Structure): + # if structure is not primitive, reduce to primitive: + primitive_structure = get_primitive_structure(composition) + if len(primitive_structure) < len(composition): + self.bulk_structure = primitive_structure + else: + self.bulk_structure = composition + self.bulk_composition = self.bulk_structure.composition + + else: + self.bulk_structure = None + self.bulk_composition = Composition(composition) - # TODO: Update installation pages, docs and tutorials - # TODO: Add tests with new API keys + self.chemsys = list(self.bulk_composition.as_dict().keys()) # get all entries in the chemical system: self.MP_full_pd_entries, self.property_key_dict, self.property_data_fields = ( @@ -993,18 +1008,24 @@ def __init__( formatted_entries = self._generate_elemental_diatomic_phases(self.MP_full_pd_entries) # get bulk entry, and warn if not stable or not present on MP database: - if bulk_entries := [ + bulk_entries = [ entry for entry in formatted_entries # sorted by e_above_hull above in get_entries_in_chemsys if entry.composition.reduced_composition == self.bulk_composition.reduced_composition - and _get_e_above_hull(entry.data) == 0.0 + ] + if zero_eah_bulk_entries := [ + entry for entry in bulk_entries if _get_e_above_hull(entry.data) == 0.0 ]: - bulk_computed_entry = bulk_entries[0] # lowest energy entry for bulk (after sorting) + self.MP_bulk_computed_entry = bulk_computed_entry = zero_eah_bulk_entries[ + 0 + ] # lowest energy entry for bulk (after sorting) else: # no EaH=0 bulk entries in pruned phase diagram, check first if present (but unstable) - if MP_bulk_entries := get_entries( # composition present in MP, but not stable + if bulk_entries := get_entries( # composition present in MP, but not stable self.bulk_composition.reduced_formula, api_key=self.api_key ): - bulk_computed_entry = MP_bulk_entries[0] # already sorted by energy in get_entries() + self.MP_bulk_computed_entry = bulk_computed_entry = bulk_entries[ + 0 + ] # already sorted by energy in get_entries() eah = PhaseDiagram(formatted_entries).get_e_above_hull(bulk_computed_entry) warnings.warn( f"Note that the Materials Project (MP) database entry for " @@ -1034,7 +1055,7 @@ def __init__( f"space, and then determine the possible competing phases with the same approach as " f"usual." ) - bulk_computed_entry = ComputedEntry( + self.MP_bulk_computed_entry = bulk_computed_entry = ComputedEntry( self.bulk_composition, self.MP_full_pd.get_hull_energy(self.bulk_composition) - 1e-4, data={ @@ -1048,16 +1069,46 @@ def __init__( ) # TODO: Later need to add handling for file writing for this (POTCAR and INCAR assuming # non-metallic, non-magnetic, with warning and recommendations - if bulk_computed_entry not in formatted_entries: - formatted_entries.append(bulk_computed_entry) - - self.MP_bulk_computed_entry = bulk_computed_entry + if self.MP_bulk_computed_entry not in formatted_entries: + formatted_entries.append(self.MP_bulk_computed_entry) + + if self.bulk_structure: # prune all bulk phases to this structure + manual_bulk_entry = None + + if bulk_entries := [ + entry + for entry in formatted_entries # sorted by e_above_hull above in get_entries_in_chemsys + if entry.composition.reduced_composition == self.bulk_composition.reduced_composition + ]: + sm = StructureMatcher() + matching_bulk_entries = [ + entry + for entry in bulk_entries + if hasattr(entry, "structure") and sm.fit(self.bulk_structure, entry.structure) + ] + matching_bulk_entries.sort(key=lambda x: sm.get_rms_dist(self.bulk_structure, x.structure)) + if matching_bulk_entries: + matching_bulk_entry = matching_bulk_entries[0] + manual_bulk_entry = matching_bulk_entry + manual_bulk_entry._structure = self.bulk_structure + + if manual_bulk_entry is None: # take the lowest energy bulk entry + manual_bulk_entry_dict = self.MP_bulk_computed_entry.as_dict() + manual_bulk_entry_dict["structure"] = self.bulk_structure.as_dict() + manual_bulk_entry = ComputedStructureEntry.from_dict(manual_bulk_entry_dict) + + formatted_entries = [ # remove bulk entries from formatted_entries and add the new bulk entry + entry + for entry in formatted_entries + if entry.composition.reduced_composition != self.bulk_composition.reduced_composition + ] + formatted_entries.append(manual_bulk_entry) if not self.full_phase_diagram: # default, prune to only phases that would border the host # material on the phase diagram, if their relative energy was downshifted by ``e_above_hull``: self.entries: list[ComputedEntry] = prune_entries_to_border_candidates( entries=formatted_entries, - bulk_computed_entry=self.MP_bulk_computed_entry, + bulk_computed_entry=bulk_computed_entry, e_above_hull=self.e_above_hull, ) @@ -1375,7 +1426,7 @@ def _generate_elemental_diatomic_phases(self, entries: list[ComputedEntry]): class ExtrinsicCompetingPhases(CompetingPhases): def __init__( self, - composition: Union[str, Composition], + composition: Union[str, Composition, Structure], extrinsic_species: Union[str, Iterable], e_above_hull: float = 0.05, full_sub_approach: bool = False, @@ -1389,9 +1440,9 @@ def __init__( potential limits for those elements in the host compound. Only extrinsic competing phases are contained in the - ``ExtrinsicCompetingPhases.entries`` list (used for input file generation), - while the `intrinsic` competing phases for the host compound are stored in - ``ExtrinsicCompetingPhases.intrinsic_entries``. + ``ExtrinsicCompetingPhases.entries`` list (used for input file + generation), while the `intrinsic` competing phases for the host + compound are stored in ``ExtrinsicCompetingPhases.intrinsic_entries``. For this, the Materials Project (MP) database is queried using the ``MPRester`` API, and any calculated compounds which `could` border @@ -1406,57 +1457,70 @@ def __init__( error of defect calculations. Args: - composition (str, Composition): - Composition of host material (e.g. 'LiFePO4', or Composition('LiFePO4'), - or Composition({"Li":1, "Fe":1, "P":1, "O":4})) + composition (str, ``Composition``, ``Structure``): + Composition of the host material (e.g. ``'LiFePO4'``, or + ``Composition('LiFePO4')``, or + ``Composition({"Li":1, "Fe":1, "P":1, "O":4})``). + Alternatively a ``pymatgen`` ``Structure`` object for the + host material can be supplied (recommended), in which case + the primitive structure will be used as the only host + composition phase, reducing the number of calculations. extrinsic_species (str, Iterable): - Extrinsic dopant/impurity species to consider, to generate the relevant - competing phases to additionally determine their chemical potential - limits within the host. Can be a single element as a string (e.g. "Mg") - or an iterable of element strings (list, set, tuple, dict) (e.g. ["Mg", - "Na"]). + Extrinsic dopant/impurity species to consider, to generate + the relevant competing phases to additionally determine their + chemical potential limits within the host. Can be a single + element as a string (e.g. "Mg") or an iterable of element + strings (list, set, tuple, dict) (e.g. ["Mg", "Na"]). e_above_hull (float): Maximum energy-above-hull of Materials Project entries to be - considered as competing phases. This is an uncertainty range for the - MP-calculated formation energies, which may not be accurate due to functional - choice (GGA vs hybrid DFT / GGA+U / RPA etc.), lack of vdW corrections etc. - Any phases that would border the host material on the phase diagram, if their - relative energy was downshifted by ``e_above_hull``, are included. + considered as competing phases. This is an uncertainty range + for the MP-calculated formation energies, which may not be + accurate due to functional choice (GGA vs hybrid DFT / GGA+U + / RPA etc.), lack of vdW corrections etc. + Any phases that would border the host material on the phase + diagram, if their relative energy was downshifted by + ``e_above_hull``, are included. Often ``e_above_hull`` can be lowered to reduce the number of - calculations while retaining good accuracy relative to the typical - error of defect calculations. + calculations while retaining good accuracy relative to the + typical error of defect calculations. Default is 0.05 eV/atom. full_sub_approach (bool): - Generate competing phases by considering the full phase diagram, including - chemical potential limits with multiple extrinsic phases. Only recommended when - looking at high (non-dilute) doping concentrations. - Default is ``False``. - The default approach (``full_sub_approach = False``) for extrinsic elements is to - only consider chemical potential limits where the host composition borders a maximum - of 1 extrinsic phase (composition with extrinsic element(s)). This is a valid - approximation for the case of dilute dopant/impurity concentrations. For high - (non-dilute) concentrations of extrinsic species, use ``full_sub_approach = True``. + Generate competing phases by considering the full phase + diagram, including chemical potential limits with multiple + extrinsic phases. Only recommended when looking at high + (non-dilute) doping concentrations. Default is ``False``. + + The default approach (``full_sub_approach = False``) for + extrinsic elements is to only consider chemical potential + limits where the host composition borders a maximum of 1 + extrinsic phase (composition with extrinsic element(s)). + This is a valid approximation for the case of dilute + dopant/impurity concentrations. For high (non-dilute) + concentrations of extrinsic species, use ``full_sub_approach = True``. codoping (bool): - Whether to consider extrinsic competing phases containing multiple - extrinsic species. Only relevant to high (non-dilute) co-doping concentrations. - If set to True, then ``full_sub_approach`` is also set to ``True``. + Whether to consider extrinsic competing phases containing + multiple extrinsic species. Only relevant to high (non-dilute) + co-doping concentrations. If set to True, then + ``full_sub_approach`` is also set to ``True``. Default is ``False``. api_key (str): - Materials Project (MP) API key, needed to access the MP database for - competing phase generation. If not supplied, will attempt to read from - environment variable ``PMG_MAPI_KEY`` (in ``~/.pmgrc.yaml``) - see the ``doped`` - Installation docs page: https://doped.readthedocs.io/en/latest/Installation.html + Materials Project (MP) API key, needed to access the MP database + for competing phase generation. If not supplied, will attempt + to read from environment variable ``PMG_MAPI_KEY`` (in + ``~/.pmgrc.yaml``) - see the ``doped`` Installation docs page: + https://doped.readthedocs.io/en/latest/Installation.html. MP API key is available at https://next-gen.materialsproject.org/api#api-key full_phase_diagram (bool): - If ``True``, include all phases on the MP phase diagram (with energy - above hull < ``e_above_hull`` eV/atom) for the chemical system of - the input composition and extrinsic species (not recommended). If ``False``, - only includes phases that would border the host material on the phase diagram - (and thus set the chemical potential limits), if their relative energy was - downshifted by ``e_above_hull`` eV/atom. - (Default is ``False``). + If ``True``, include all phases on the MP phase diagram (with + energy above hull < ``e_above_hull`` eV/atom) for the chemical + system of the input composition and extrinsic species (not + recommended). If ``False``, only includes phases that would border + the host material on the phase diagram (and thus set the chemical + potential limits), if their relative energy was downshifted by + ``e_above_hull`` eV/atom. + Default is ``False``. """ super().__init__( # competing phases & entries of the OG system: composition=composition, diff --git a/doped/utils/symmetry.py b/doped/utils/symmetry.py index 838067e3..8f2083d2 100644 --- a/doped/utils/symmetry.py +++ b/doped/utils/symmetry.py @@ -5,7 +5,7 @@ import contextlib import os import warnings -from typing import Optional +from typing import Optional, Union import numpy as np from pymatgen.analysis.defects.core import DefectType @@ -551,10 +551,15 @@ def get_clean_structure(structure: Structure, return_T: bool = False): return new_structure -def get_primitive_structure(sga, ignored_species: Optional[list] = None, clean: bool = True): +def get_primitive_structure( + sga_or_struct: Union[SpacegroupAnalyzer, Structure], + ignored_species: Optional[list] = None, + clean: bool = True, + **kwargs, +): """ Get a consistent/deterministic primitive structure from a - SpacegroupAnalyzer object. + ``SpacegroupAnalyzer`` object, or ``pymatgen`` ``Structure``. For some materials (e.g. zinc blende), there are multiple equivalent primitive cells, so for reproducibility and in line with most structure @@ -562,10 +567,28 @@ def get_primitive_structure(sga, ignored_species: Optional[list] = None, clean: fractional coordinates of the sites (i.e. favour Cd (0,0,0) and Te (0.25,0.25,0.25) over Cd (0,0,0) and Te (0.75,0.75,0.75) for F-43m CdTe). - If ignored_species is set, then the sorting function used to determine the + If ``ignored_species`` is set, then the sorting function used to determine the ideal primitive structure will ignore sites with species in - ignored_species. - """ + ``ignored_species``. + + Args: + sga_or_struct: + ``SpacegroupAnalyzer`` object or ``Structure`` object to get the + corresponding primitive structure of. If a ``Structure`` object, + then additional kwargs are passed to the ``_get_sga`` function + which obtains the ``SpacegroupAnalyzer`` object for this structure. + ignored_species: + List of species to ignore when determining the ideal primitive + structure. (Default: None) + clean: + Whether to return a 'clean' version of the primitive structure, + with the lattice matrix in a standardised form. (Default: True) + **kwargs: + Additional keyword arguments to pass to the ``_get_sga`` function + (e.g. ``symprec`` etc). + """ + sga = _get_sga(sga_or_struct, **kwargs) if isinstance(sga_or_struct, Structure) else sga_or_struct + possible_prim_structs = [] for _i in range(4): struct = sga.get_primitive_standard_structure() diff --git a/tests/data/Cu2SiSe3_MP_POSCAR b/tests/data/Cu2SiSe3_MP_POSCAR new file mode 100644 index 00000000..aeae0e7a --- /dev/null +++ b/tests/data/Cu2SiSe3_MP_POSCAR @@ -0,0 +1,32 @@ +Cu8 Si4 Se12 +1.0 + 0.0000000000000000 6.6709767058199212 0.0000000000000000 + 11.7236801335501752 0.0000000000000000 0.0000000000000000 + 0.0000000000000000 -2.1177252581368129 -6.3413307211506247 +Cu Si Se +8 4 12 +direct + 0.9879925050000000 0.7548038650000000 0.4973765800000000 Cu+ + 0.4879925050000000 0.7451961350000000 0.9973765800000001 Cu+ + 0.4970841949999999 0.9130037750000000 0.4998422500000000 Cu+ + 0.9970841949999998 0.5869962250000000 0.9998422499999999 Cu+ + 0.4879925050000000 0.2548038650000000 0.4973765800000000 Cu+ + 0.9879925050000000 0.2451961350000000 0.9973765800000001 Cu+ + 0.9970841949999998 0.4130037750000000 0.4998422500000000 Cu+ + 0.4970841949999998 0.0869962250000000 0.9998422499999999 Cu+ + 0.4706155300000000 0.5848629399999999 0.4756780200000000 Si4+ + 0.9706155299999999 0.9151370600000001 0.9756780200000001 Si4+ + 0.9706155299999999 0.0848629399999999 0.4756780200000000 Si4+ + 0.4706155299999999 0.4151370600000001 0.9756780200000001 Si4+ + 0.6139464749999999 0.9152577350000000 0.8774259200000000 Se2- + 0.1139464749999999 0.5847422650000000 0.3774259200000000 Se2- + 0.6090688300000000 0.7419345700000000 0.3731250300000000 Se2- + 0.1090688300000000 0.7580654300000000 0.8731250300000000 Se2- + 0.6091879750000000 0.5785419649999999 0.8550521899999999 Se2- + 0.1091879750000000 0.9214580350000001 0.3550521899999999 Se2- + 0.1139464749999999 0.4152577350000000 0.8774259200000000 Se2- + 0.6139464749999999 0.0847422650000000 0.3774259200000000 Se2- + 0.1090688300000000 0.2419345700000000 0.3731250300000000 Se2- + 0.6090688300000000 0.2580654300000000 0.8731250300000000 Se2- + 0.1091879750000000 0.0785419649999999 0.8550521899999999 Se2- + 0.6091879750000000 0.4214580350000001 0.3550521899999999 Se2- diff --git a/tests/data/Na2FePO4F_MP_POSCAR b/tests/data/Na2FePO4F_MP_POSCAR new file mode 100644 index 00000000..d764369c --- /dev/null +++ b/tests/data/Na2FePO4F_MP_POSCAR @@ -0,0 +1,80 @@ +Na16 Fe8 P8 O32 F8 +1.0 + 5.2809960000000000 0.0000000000000000 0.0000000000000003 + 0.0000000000000019 11.9541699999999995 0.0000000000000007 + 0.0000000000000000 0.0000000000000000 14.0416679999999996 +Na Fe P O F +16 8 8 32 8 +direct + 0.2637200000000000 0.3282100000000000 0.2447880000000001 Na+ + 0.2362800000000000 0.8282100000000000 0.2552119999999999 Na+ + 0.7637200000000000 0.6717900000000000 0.2552119999999999 Na+ + 0.7362800000000000 0.1717900000000000 0.2447880000000001 Na+ + 0.7362800000000000 0.6717900000000000 0.7552119999999999 Na+ + 0.7637200000000000 0.1717900000000000 0.7447880000000001 Na+ + 0.2362800000000000 0.3282100000000000 0.7447880000000001 Na+ + 0.2637200000000000 0.8282100000000000 0.7552119999999999 Na+ + 0.2391560000000000 0.0834260000000000 0.1247590000000001 Na+ + 0.2608440000000000 0.5834260000000000 0.3752409999999999 Na+ + 0.7391560000000000 0.9165740000000000 0.3752409999999999 Na+ + 0.7608440000000000 0.4165740000000000 0.1247590000000001 Na+ + 0.7608440000000000 0.9165740000000000 0.8752409999999999 Na+ + 0.7391560000000000 0.4165740000000000 0.6247590000000001 Na+ + 0.2608440000000000 0.0834260000000000 0.6247590000000001 Na+ + 0.2391560000000000 0.5834260000000000 0.8752409999999999 Na+ + 0.2277400000000000 0.3257140000000000 0.0098220000000001 Fe2+ + 0.2722600000000000 0.8257140000000001 0.4901779999999999 Fe2+ + 0.7277400000000001 0.6742859999999999 0.4901779999999999 Fe2+ + 0.7722599999999999 0.1742860000000000 0.0098220000000001 Fe2+ + 0.7722599999999999 0.6742859999999999 0.9901779999999999 Fe2+ + 0.7277400000000001 0.1742860000000000 0.5098220000000001 Fe2+ + 0.2722600000000000 0.3257140000000000 0.5098220000000001 Fe2+ + 0.2277400000000000 0.8257140000000001 0.9901779999999999 Fe2+ + 0.2035070000000000 0.0869920000000000 0.3807690000000000 P5+ + 0.2964930000000000 0.5869920000000000 0.1192310000000000 P5+ + 0.7035070000000000 0.9130080000000000 0.1192310000000000 P5+ + 0.7964930000000000 0.4130080000000000 0.3807690000000000 P5+ + 0.7964930000000000 0.9130080000000000 0.6192310000000000 P5+ + 0.7035070000000000 0.4130080000000000 0.8807690000000000 P5+ + 0.2964930000000000 0.0869920000000000 0.8807690000000000 P5+ + 0.2035070000000000 0.5869920000000000 0.6192310000000000 P5+ + 0.2669350000000000 0.9602830000000000 0.3876400000000001 O2- + 0.2330650000000000 0.4602830000000000 0.1123599999999999 O2- + 0.7669349999999999 0.0397170000000000 0.1123599999999999 O2- + 0.7330650000000001 0.5397170000000000 0.3876400000000001 O2- + 0.7330650000000001 0.0397170000000000 0.6123599999999999 O2- + 0.7669349999999999 0.5397170000000000 0.8876400000000001 O2- + 0.2330650000000000 0.9602830000000000 0.8876400000000001 O2- + 0.2669350000000000 0.4602830000000000 0.6123599999999999 O2- + 0.2851730000000000 0.1334270000000000 0.2834150000000001 O2- + 0.2148270000000000 0.6334270000000000 0.2165849999999999 O2- + 0.7851730000000000 0.8665730000000000 0.2165849999999999 O2- + 0.7148270000000000 0.3665730000000000 0.2834150000000001 O2- + 0.7148270000000000 0.8665730000000000 0.7165849999999999 O2- + 0.7851730000000000 0.3665730000000000 0.7834150000000001 O2- + 0.2148270000000000 0.1334270000000000 0.7834150000000001 O2- + 0.2851730000000000 0.6334270000000000 0.7165849999999999 O2- + 0.9087130000000000 0.1026060000000000 0.3943770000000001 O2- + 0.5912870000000000 0.6026060000000000 0.1056229999999999 O2- + 0.4087130000000001 0.8973940000000000 0.1056229999999999 O2- + 0.0912870000000000 0.3973940000000000 0.3943770000000001 O2- + 0.0912870000000000 0.8973940000000000 0.6056229999999999 O2- + 0.4087130000000001 0.3973940000000000 0.8943770000000001 O2- + 0.5912870000000000 0.1026060000000000 0.8943770000000001 O2- + 0.9087130000000000 0.6026060000000000 0.6056229999999999 O2- + 0.3390480000000000 0.1511320000000000 0.4636200000000000 O2- + 0.1609520000000000 0.6511320000000000 0.0363800000000000 O2- + 0.8390480000000000 0.8488680000000000 0.0363800000000000 O2- + 0.6609520000000000 0.3488680000000000 0.4636200000000000 O2- + 0.6609520000000000 0.8488680000000000 0.5363800000000000 O2- + 0.8390480000000000 0.3488680000000000 0.9636200000000000 O2- + 0.1609520000000000 0.1511320000000000 0.9636200000000000 O2- + 0.3390480000000000 0.6511320000000000 0.5363800000000000 O2- + 0.0000000000000000 0.2500000000000000 0.1241310000000001 F- + 0.5000000000000000 0.7500000000000000 0.3758689999999999 F- + 0.0000000000000000 0.7500000000000000 0.8758689999999999 F- + 0.5000000000000000 0.2500000000000000 0.6241310000000001 F- + 0.5000000000000000 0.2500000000000000 0.1012330000000001 F- + 0.0000000000000000 0.7500000000000000 0.3987669999999999 F- + 0.5000000000000000 0.7500000000000000 0.8987669999999999 F- + 0.0000000000000000 0.2500000000000000 0.6012330000000001 F- diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index 316eb5e8..e455a342 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -18,6 +18,7 @@ from test_analysis import if_present_rm from doped import chemical_potentials +from doped.utils.symmetry import get_primitive_structure def _compare_chempot_dicts(dict1, dict2): @@ -54,6 +55,13 @@ def setUp(self) -> None: self.legacy_api_key = "c2LiJRMiBeaN5iXsH" # SK MP Imperial email (GitHub) A/C self.api_key = "UsPX9Hwut4drZQXPTxk4CwlCstrAAjDv" # SK MP Imperial email (GitHub) A/C + self.cdte = Structure.from_file("../examples/CdTe/relaxed_primitive_POSCAR") + self.na2fepo4f = Structure.from_file("data/Na2FePO4F_MP_POSCAR") + self.cu2sise3 = Structure.from_file("data/Cu2SiSe3_MP_POSCAR") + self.cu2sise4 = self.cu2sise3.get_primitive_structure().copy() + self.cu2sise4.append("Se", [0.5, 0.5, 0.5]) + self.cu2sise4.append("Se", [0.5, 0.75, 0.5]) + def tearDown(self) -> None: if_present_rm("CompetingPhases") @@ -410,6 +418,63 @@ def test_legacy_API_message(self): "-project-api" ) in output + @parameterized_subtest() + def test_structure_input(self, api_key): + for struct, name in [ + (self.cdte, "CdTe_F-43m_EaH_0"), + (self.cdte * 2, "CdTe_F-43m_EaH_0"), # supercell + (self.na2fepo4f, "Na2FePO4F_Pbcn_EaH_0.17"), + (self.cu2sise4, "Cu2SiSe4_P1_EaH_0"), + ]: + with warnings.catch_warnings(record=True) as w: + cp = chemical_potentials.CompetingPhases( + struct.composition.reduced_formula, api_key=api_key + ) + cp_struct_input = chemical_potentials.CompetingPhases(struct, api_key=api_key) + + _check_structure_input(cp, cp_struct_input, struct, name, w, api_key) + + +def _check_structure_input(cp, cp_struct_input, struct, name, w, api_key, extrinsic=False): + print([str(warning.message) for warning in w]) # for debugging + user_warnings = [warning for warning in w if warning.category is UserWarning] + if "Na2FePO4F" in name and len(api_key) != 32: # stable in new MP + assert len(user_warnings) == 2 + assert "Note that the Materials Project (MP) database entry for Na2FePO4F is not stable" in str( + user_warnings[0].message + ) + elif "Cu2SiSe4" in name: + assert len(user_warnings) == 2 + assert "Note that no Materials Project (MP) database entry exists for Cu2SiSe4" in str( + user_warnings[0].message + ) + else: + assert not user_warnings + + struct_entries = cp_struct_input.entries if not extrinsic else cp_struct_input.intrinsic_entries + cp_entries = cp.entries if not extrinsic else cp.intrinsic_entries + for entry in struct_entries: + if entry.name != "Cu2SiSe4": # differs in this case due to doubled formula in unit cell + assert entry in cp_entries # structure not compared with ``__eq__`` for entries + if entry.name == struct.composition.reduced_formula: + if "Na2FePO4F" not in name or len(api_key) != 32: + assert entry.data["doped_name"] == name + else: + assert entry.data["doped_name"] == "Na2FePO4F_Pbcn_EaH_0" # stable in new MP + if entry.name != "CdTe" or len(struct) != 16: + assert entry.structure == struct + else: # with supercell input, structure reduced to the primitive cell + assert entry.structure == get_primitive_structure(struct) + + for entry in cp_entries: + if entry.name != struct.composition.reduced_formula: + assert entry in struct_entries + + assert len(struct_entries) <= len(cp_entries) + assert ( + len([entry for entry in struct_entries if entry.name == struct.composition.reduced_formula]) == 1 + ) + class ExtrinsicCompetingPhasesTestCase(unittest.TestCase): # same setUp and tearDown as above # TODO: Need to add tests for co-doping, full_sub_approach, full_phase_diagram etc!! @@ -446,6 +511,30 @@ def test_init(self, api_key): assert all(entry.data["e_above_hull"] != 0 for entry in cp.entries[2:]) assert len(cp.intrinsic_entries) == 18 + @parameterized_subtest() + def test_structure_input(self, api_key): + for struct, name in [ + (self.cdte, "CdTe_F-43m_EaH_0"), + (self.cdte * 2, "CdTe_F-43m_EaH_0"), # supercell + (self.na2fepo4f, "Na2FePO4F_Pbcn_EaH_0.17"), + (self.cu2sise4, "Cu2SiSe4_P1_EaH_0"), + ]: + with warnings.catch_warnings(record=True) as w: + cp = chemical_potentials.ExtrinsicCompetingPhases( + struct.composition.reduced_formula, api_key=api_key, extrinsic_species={"K"} + ) + cp_struct_input = chemical_potentials.ExtrinsicCompetingPhases( + struct, api_key=api_key, extrinsic_species={"K"} + ) + + _check_structure_input(cp, cp_struct_input, struct, name, w, api_key, extrinsic=True) + + for entries_list in [cp_struct_input.entries, cp.entries]: + assert len(entries_list) >= 1 + for extrinsic_entry in entries_list: + assert "K" in extrinsic_entry.data["doped_name"] + assert "K" in extrinsic_entry.name + class ChemPotAnalyzerTestCase(unittest.TestCase): def setUp(self): From 1f6bc8c1cdf3122474f11b753f937c6d9ef1940c Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Wed, 7 Aug 2024 12:14:33 -0400 Subject: [PATCH 16/17] Notes and minor cleanup --- docs/Dev_ToDo.md | 4 +++- docs/Future_ToDo.md | 2 +- doped/thermodynamics.py | 2 +- tests/test_chemical_potentials.py | 10 +++++----- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/Dev_ToDo.md b/docs/Dev_ToDo.md index cd299b06..0895d4d7 100644 --- a/docs/Dev_ToDo.md +++ b/docs/Dev_ToDo.md @@ -1,6 +1,6 @@ # `doped` Development To-Do List ## Chemical potential -- Check through chemical potential TO-DOs. Need to recheck validity of approximations used for extrinsic competing phases (and code for this). Proper `vasp_std` setup (with `NKRED` folders like for defect calcs) and `vasp_ncl` generation. +- Need to recheck validity of approximations used for extrinsic competing phases (and code for this). Proper `vasp_std` setup (with `NKRED` folders like for defect calcs) and `vasp_ncl` generation. - Efficient generation of competing phases for which there are many polymorphs? See SK notes from CdTe competing phases. - Publication ready chemical potential diagram plotting tool as in Adam Jackson's `plot-cplap-ternary` (3D) and Sungyhun's `cplapy` (4D) (see `doped_chempot_plotting_example.ipynb`; code there, just needs to be implemented in module functions). `ChemicalPotentialGrid` in `py-sc-fermi` interface could be quite useful for this? (Worth moving that part of code out of `interface` subpackage?) Also can see https://github.com/materialsproject/pymatgen-analysis-defects/blob/main/pymatgen/analysis/defects/plotting/phases.py for 2D chempot plotting. - Also see `Cs2SnTiI6` notebooks for template code for this. @@ -15,6 +15,8 @@ - Could also add an optional right-hand-side y-axis for defect concentration (for a chosen anneal temp) to our TLD plotting (e.g. `concentration_T = None`) as done for thesis, noting in docstring that this obvs doesn't account for degeneracy! - Separate `dist_tol` for interstitials vs (inequivalent) vacancies/substitutions? (See Xinwei chat) Any other options on this front? - Also see Fig. 6a of the `AiiDA-defects` preprint, want plotting tools like this +- Kumagai GKFO and CC diagram corrections. Implemented in `pydefect` and relatively easy to port? +- 2D corrections; like `pydefect_2d`, or recommended to use SCPC in VASP? - Can we add an option to give the `pydefect` defect-structure-info output (shown here https://kumagai-group.github.io/pydefect/tutorial.html#check-defect-structures) – seems quite useful tbf ## Housekeeping diff --git a/docs/Future_ToDo.md b/docs/Future_ToDo.md index 875579b6..cfb1bbfe 100644 --- a/docs/Future_ToDo.md +++ b/docs/Future_ToDo.md @@ -84,9 +84,9 @@ - In these cases, will also want to be able to plot these in a smart manner on the defect TLD. Separate lines to the stoichiometrically-equivalent (unperturbed) point defect, but with the same colour just different linestyles? (or something similar) -- `doped` functions should be equally applicable to the base `pymatgen` `Defect`/`DefectEntry` objects (as well as the corresponding `doped` subclasses) as much as possible. Can we add some quick tests for this? - Implement shallow donor/acceptor binding estimation functions (via effective mass theory) - Kasamatsu formula for defect concentrations at high concentrations (accounts for lattice site competition), as shown in DefAP paper +- Eigenvalue corrections for the eigenvalue plots, like shown in 10.1103/PhysRevB.109.054106? ## Docs - Add LDOS plotting, big selling point for defects and disorder! diff --git a/doped/thermodynamics.py b/doped/thermodynamics.py index 75b0694d..bc04de88 100644 --- a/doped/thermodynamics.py +++ b/doped/thermodynamics.py @@ -430,7 +430,7 @@ def __init__( plots. Args: - defect_entries ([DefectEntry] or {str: DefectEntry}): + defect_entries (list[DefectEntry] or dict[str, DefectEntry]): A list or dict of DefectEntry objects. Note that ``DefectEntry.name`` attributes are used for grouping and plotting purposes! These should be in the format "{defect_name}_{optional_site_info}_{charge_state}". diff --git a/tests/test_chemical_potentials.py b/tests/test_chemical_potentials.py index e455a342..e2387543 100644 --- a/tests/test_chemical_potentials.py +++ b/tests/test_chemical_potentials.py @@ -529,11 +529,11 @@ def test_structure_input(self, api_key): _check_structure_input(cp, cp_struct_input, struct, name, w, api_key, extrinsic=True) - for entries_list in [cp_struct_input.entries, cp.entries]: - assert len(entries_list) >= 1 - for extrinsic_entry in entries_list: - assert "K" in extrinsic_entry.data["doped_name"] - assert "K" in extrinsic_entry.name + for entries_list in [cp_struct_input.entries, cp.entries]: + assert len(entries_list) >= 1 + for extrinsic_entry in entries_list: + assert "K" in extrinsic_entry.data["doped_name"] + assert "K" in extrinsic_entry.name class ChemPotAnalyzerTestCase(unittest.TestCase): From b3c674d934c29b66bfa885dd538ecf43709052d4 Mon Sep 17 00:00:00 2001 From: Sean Kavanagh Date: Thu, 15 Aug 2024 19:21:29 -0400 Subject: [PATCH 17/17] Add `get_doped_chempots_from_entries` convenience function to `doped.chemical_potentials` --- doped/chemical_potentials.py | 114 +++++++++++++++++++++++++---------- 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/doped/chemical_potentials.py b/doped/chemical_potentials.py index 2bb1131a..30ebaaed 100644 --- a/doped/chemical_potentials.py +++ b/doped/chemical_potentials.py @@ -9,7 +9,7 @@ import itertools import os import warnings -from collections.abc import Iterable +from collections.abc import Iterable, Sequence from copy import deepcopy from pathlib import Path from typing import Any, Optional, Union @@ -1681,6 +1681,77 @@ def __init__( _name_entries_and_handle_duplicates(self.entries) # set entry names +def get_doped_chempots_from_entries( + entries: Sequence[Union[ComputedEntry, ComputedStructureEntry, PDEntry]], + composition: Union[str, Composition, ComputedEntry], + sort_by: Optional[str] = None, + single_chempot_limit: bool = False, +) -> dict: + r""" + Given a list of ``ComputedEntry``\s / ``ComputedStructureEntry``\s / + ``PDEntry``\s and the bulk ``composition``, returns the chemical potential + limits dictionary in the ``doped`` format (i.e. ``{"limits": [{'limit': + [chempot_dict]}], ...}``) for the host material. + + Args: + entries (list[ComputedEntry]): + List of ``ComputedEntry``\s / ``ComputedStructureEntry``\s / + ``PDEntry``\s for the chemical system, from which to determine + the chemical potential limits for the host material (``composition``). + composition (str, Composition, ComputedEntry): + Composition of the host material either as a string + (e.g. 'LiFePO4') a ``pymatgen`` ``Composition`` object (e.g. + ``Composition('LiFePO4')``), or a ``ComputedEntry`` object. + sort_by (str): + If set, will sort the chemical potential limits in the output + ``DataFrame`` according to the chemical potential of the specified + element (from element-rich to element-poor conditions). + single_chempot_limit (bool): + If set to ``True``, only returns the first chemical potential limit + in the calculated chemical potentials dictionary. Mainly intended for + internal ``doped`` usage when the host material is calculated to be + unstable with respect to the competing phases. + + Returns: + dict: + Dictionary of chemical potential limits in the ``doped`` format. + """ + if isinstance(composition, (str, Composition)): + composition = Composition(composition) + else: + composition = composition.composition + + phase_diagram = PhaseDiagram( + entries, + list(map(Element, composition.elements)), # preserve bulk comp element ordering + ) + chem_lims = phase_diagram.get_all_chempots(composition.reduced_composition) + chem_lims_iterator = list(chem_lims.items())[:1] if single_chempot_limit else chem_lims.items() + + # remove Element to make it JSONable: + no_element_chem_lims = {k: {str(kk): vv for kk, vv in v.items()} for k, v in chem_lims_iterator} + + if sort_by is not None: + no_element_chem_lims = dict( + sorted(no_element_chem_lims.items(), key=lambda x: x[1][sort_by], reverse=True) + ) + + chempots = { + "limits": no_element_chem_lims, + "elemental_refs": {str(el): ent.energy_per_atom for el, ent in phase_diagram.el_refs.items()}, + "limits_wrt_el_refs": {}, + } + + # relate the limits to the elemental energies + for limit, chempot_dict in chempots["limits"].items(): + relative_chempot_dict = copy.deepcopy(chempot_dict) + for e in relative_chempot_dict: + relative_chempot_dict[e] -= chempots["elemental_refs"][e] + chempots["limits_wrt_el_refs"].update({limit: relative_chempot_dict}) + + return chempots + + class CompetingPhasesAnalyzer: # TODO: Allow parsing using pymatgen ComputedEntries as well, to aid interoperability with # high-througput architectures like AiiDA or atomate2. See: @@ -2151,7 +2222,7 @@ def calculate_chempots( self._intrinsic_phase_diagram = PhaseDiagram( intrinsic_phase_diagram_entries, - map(Element, self.bulk_composition.elements), + list(map(Element, self.bulk_composition.elements)), # preserve bulk comp element ordering ) # check if it's stable and if not, warn user and downshift to get _least_ unstable point on convex @@ -2179,38 +2250,15 @@ def calculate_chempots( ) self._intrinsic_phase_diagram = PhaseDiagram( [*intrinsic_phase_diagram_entries, renormalised_bulk_pde], - map(Element, self.bulk_composition.elements), + list(map(Element, self.bulk_composition.elements)), # preserve bulk comp element ordering ) - chem_lims = self._intrinsic_phase_diagram.get_all_chempots(self.bulk_composition) - - # remove Element to make it JSONable: - no_element_chem_lims = {k: {str(kk): vv for kk, vv in v.items()} for k, v in chem_lims.items()} - - if unstable_host: - no_element_chem_lims = { - k: {str(kk): vv for kk, vv in v.items()} for k, v in list(chem_lims.items())[:1] - } - - if sort_by is not None: - no_element_chem_lims = dict( - sorted(no_element_chem_lims.items(), key=lambda x: x[1][sort_by], reverse=True) - ) - - self._intrinsic_chempots = { - "limits": no_element_chem_lims, - "elemental_refs": { - str(el): ent.energy_per_atom for el, ent in self._intrinsic_phase_diagram.el_refs.items() - }, - "limits_wrt_el_refs": {}, - } - - # relate the limits to the elemental energies - for limit, chempot_dict in self._intrinsic_chempots["limits"].items(): - relative_chempot_dict = copy.deepcopy(chempot_dict) - for e in relative_chempot_dict: - relative_chempot_dict[e] -= self._intrinsic_chempots["elemental_refs"][e] - self._intrinsic_chempots["limits_wrt_el_refs"].update({limit: relative_chempot_dict}) + self._intrinsic_chempots = get_doped_chempots_from_entries( + self._intrinsic_phase_diagram.entries, + self.bulk_composition, + sort_by=sort_by, + single_chempot_limit=unstable_host, + ) # get chemical potentials as pandas dataframe chemical_potentials = [] @@ -2340,7 +2388,7 @@ def intrinsic_chempots(self) -> dict: return self._intrinsic_chempots @property - def intrinsic_phase_diagram(self) -> dict: + def intrinsic_phase_diagram(self) -> PhaseDiagram: """ Returns the calculated intrinsic phase diagram. """