def get_pourbaix_entries(self, chemsys): """ A helper function to get all entries necessary to generate a pourbaix diagram from the rest interface. Args: chemsys ([str]): A list of elements comprising the chemical system, e.g. ['Li', 'Fe'] """ from pymatgen.analysis.pourbaix.entry import PourbaixEntry, IonEntry from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.core.ion import Ion from pymatgen.entries.compatibility import\ MaterialsProjectAqueousCompatibility chemsys = list(set(chemsys + ['O', 'H'])) entries = self.get_entries_in_chemsys(chemsys, property_data=['e_above_hull'], compatible_only=False) compat = MaterialsProjectAqueousCompatibility("Advanced") entries = compat.process_entries(entries) solid_pd = PhaseDiagram( entries) # Need this to get ion formation energy url = '/pourbaix_diagram/reference_data/' + '-'.join(chemsys) ion_data = self._make_request(url) pbx_entries = [] for entry in entries: if not set(entry.composition.elements)\ <= {Element('H'), Element('O')}: pbx_entry = PourbaixEntry(entry) pbx_entry.g0_replace(solid_pd.get_form_energy(entry)) pbx_entry.reduced_entry() pbx_entries.append(pbx_entry) # position the ion energies relative to most stable reference state for n, i_d in enumerate(ion_data): ion_entry = IonEntry(Ion.from_formula(i_d['Name']), i_d['Energy']) refs = [ e for e in entries if e.composition.reduced_formula == i_d['Reference Solid'] ] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.data['e_above_hull'])[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] solid_diff = solid_pd.get_form_energy(stable_ref)\ - i_d['Reference solid energy'] * rf elt = i_d['Major_Elements'][0] correction_factor = ion_entry.ion.composition[elt]\ / stable_ref.composition[elt] correction = solid_diff * correction_factor pbx_entries.append( PourbaixEntry(ion_entry, correction, 'ion-{}'.format(n))) return pbx_entries
def get_pourbaix_entries(self, chemsys): """ A helper function to get all entries necessary to generate a pourbaix diagram from the rest interface. Args: chemsys ([str]): A list of elements comprising the chemical system, e.g. ['Li', 'Fe'] """ from pymatgen.analysis.pourbaix.entry import PourbaixEntry, IonEntry from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.core.ion import Ion from pymatgen.entries.compatibility import\ MaterialsProjectAqueousCompatibility chemsys = list(set(chemsys + ['O', 'H'])) entries = self.get_entries_in_chemsys( chemsys, property_data=['e_above_hull'], compatible_only=False) compat = MaterialsProjectAqueousCompatibility("Advanced") entries = compat.process_entries(entries) solid_pd = PhaseDiagram(entries) # Need this to get ion formation energy url = '/pourbaix_diagram/reference_data/' + '-'.join(chemsys) ion_data = self._make_request(url) pbx_entries = [] for entry in entries: if not set(entry.composition.elements)\ <= {Element('H'), Element('O')}: pbx_entry = PourbaixEntry(entry) pbx_entry.g0_replace(solid_pd.get_form_energy(entry)) pbx_entry.reduced_entry() pbx_entries.append(pbx_entry) # position the ion energies relative to most stable reference state for n, i_d in enumerate(ion_data): ion_entry = IonEntry(Ion.from_formula(i_d['Name']), i_d['Energy']) refs = [e for e in entries if e.composition.reduced_formula == i_d['Reference Solid']] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.data['e_above_hull'])[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] solid_diff = solid_pd.get_form_energy(stable_ref)\ - i_d['Reference solid energy'] * rf elt = i_d['Major_Elements'][0] correction_factor = ion_entry.ion.composition[elt]\ / stable_ref.composition[elt] correction = solid_diff * correction_factor pbx_entries.append(PourbaixEntry(ion_entry, correction, 'ion-{}'.format(n))) return pbx_entries
def test_get_ion_entries(self, mpr): entries = mpr.get_entries_in_chemsys("Ti-O-H") pd = PhaseDiagram(entries) ion_entry_data = mpr.get_ion_reference_data_for_chemsys("Ti-O-H") ion_entries = mpr.get_ion_entries(pd, ion_entry_data) assert len(ion_entries) == 5 assert all([isinstance(i, IonEntry) for i in ion_entries]) # test an incomplete phase diagram entries = mpr.get_entries_in_chemsys("Ti-O") pd = PhaseDiagram(entries) with pytest.raises(ValueError, match="The phase diagram chemical system"): mpr.get_ion_entries(pd) # test ion energy calculation ion_data = mpr.get_ion_reference_data_for_chemsys('S') ion_ref_comps = [ Ion.from_formula(d["data"]["RefSolid"]).composition for d in ion_data ] ion_ref_elts = set( itertools.chain.from_iterable(i.elements for i in ion_ref_comps)) ion_ref_entries = mpr.get_entries_in_chemsys( list([str(e) for e in ion_ref_elts] + ["O", "H"])) mpc = MaterialsProjectAqueousCompatibility() ion_ref_entries = mpc.process_entries(ion_ref_entries) ion_ref_pd = PhaseDiagram(ion_ref_entries) ion_entries = mpr.get_ion_entries(ion_ref_pd, ion_ref_data=ion_data) # In ion ref data, SO4-2 is -744.27 kJ/mol; ref solid is -1,279.0 kJ/mol # so the ion entry should have an energy (-744.27 +1279) = 534.73 kJ/mol # or 5.542 eV/f.u. above the energy of Na2SO4 so4_two_minus = [ e for e in ion_entries if e.ion.reduced_formula == "SO4[-2]" ][0] # the ref solid is Na2SO4, ground state mp-4770 # the rf factor correction is necessary to make sure the composition # of the reference solid is normalized to a single formula unit ref_solid_entry = [ e for e in ion_ref_entries if e.entry_id == 'mp-4770' ][0] rf = ref_solid_entry.composition.get_reduced_composition_and_factor( )[1] solid_energy = ion_ref_pd.get_form_energy(ref_solid_entry) / rf assert np.allclose(so4_two_minus.energy, solid_energy + 5.542, atol=1e-3)
class PhaseDiagramTest(unittest.TestCase): def setUp(self): self.entries = EntrySet.from_csv(str(module_dir / "pdentries_test.csv")) self.pd = PhaseDiagram(self.entries) warnings.simplefilter("ignore") def tearDown(self): warnings.simplefilter("default") def test_init(self): # Ensure that a bad set of entries raises a PD error. Remove all Li # from self.entries. entries = filter( lambda e: (not e.composition.is_element) or e.composition.elements[ 0] != Element("Li"), self.entries, ) self.assertRaises(PhaseDiagramError, PhaseDiagram, entries) def test_dim1(self): # Ensure that dim 1 PDs can eb generated. for el in ["Li", "Fe", "O2"]: entries = [ e for e in self.entries if e.composition.reduced_formula == el ] pd = PhaseDiagram(entries) self.assertEqual(len(pd.stable_entries), 1) for e in entries: decomp, ehull = pd.get_decomp_and_e_above_hull(e) self.assertGreaterEqual(ehull, 0) plotter = PDPlotter(pd) lines, stable_entries, unstable_entries = plotter.pd_plot_data self.assertEqual(lines[0][1], [0, 0]) def test_ordering(self): # Test sorting of elements entries = [ ComputedEntry(Composition(formula), 0) for formula in ["O", "N", "Fe"] ] pd = PhaseDiagram(entries) sorted_elements = (Element("Fe"), Element("N"), Element("O")) self.assertEqual(tuple(pd.elements), sorted_elements) entries.reverse() pd = PhaseDiagram(entries) self.assertEqual(tuple(pd.elements), sorted_elements) # Test manual specification of order ordering = [Element(elt_string) for elt_string in ["O", "N", "Fe"]] pd = PhaseDiagram(entries, elements=ordering) self.assertEqual(tuple(pd.elements), tuple(ordering)) def test_stable_entries(self): stable_formulas = [ ent.composition.reduced_formula for ent in self.pd.stable_entries ] expected_stable = [ "Fe2O3", "Li5FeO4", "LiFeO2", "Fe3O4", "Li", "Fe", "Li2O", "O2", "FeO", ] for formula in expected_stable: self.assertTrue(formula in stable_formulas, formula + " not in stable entries!") def test_get_formation_energy(self): stable_formation_energies = { ent.composition.reduced_formula: self.pd.get_form_energy(ent) for ent in self.pd.stable_entries } expected_formation_energies = { "Li5FeO4": -164.8117344866667, "Li2O2": -14.119232793333332, "Fe2O3": -16.574164339999996, "FeO": -5.7141519966666685, "Li": 0.0, "LiFeO2": -7.732752316666666, "Li2O": -6.229303868333332, "Fe": 0.0, "Fe3O4": -22.565714456666683, "Li2FeO3": -45.67166036000002, "O2": 0.0, } for formula, energy in expected_formation_energies.items(): self.assertAlmostEqual(energy, stable_formation_energies[formula], 7) def test_all_entries_hulldata(self): self.assertEqual(len(self.pd.all_entries_hulldata), 492) def test_planar_inputs(self): e1 = PDEntry("H", 0) e2 = PDEntry("He", 0) e3 = PDEntry("Li", 0) e4 = PDEntry("Be", 0) e5 = PDEntry("B", 0) e6 = PDEntry("Rb", 0) pd = PhaseDiagram([e1, e2, e3, e4, e5, e6], map(Element, ["Rb", "He", "B", "Be", "Li", "H"])) self.assertEqual(len(pd.facets), 1) def test_str(self): self.assertIsNotNone(str(self.pd)) def test_get_e_above_hull(self): for entry in self.pd.stable_entries: self.assertLess( self.pd.get_e_above_hull(entry), 1e-11, "Stable entries should have e above hull of zero!", ) for entry in self.pd.all_entries: if entry not in self.pd.stable_entries: e_ah = self.pd.get_e_above_hull(entry) self.assertTrue(isinstance(e_ah, Number)) self.assertGreaterEqual(e_ah, 0) def test_get_equilibrium_reaction_energy(self): for entry in self.pd.stable_entries: self.assertLessEqual( self.pd.get_equilibrium_reaction_energy(entry), 0, "Stable entries should have negative equilibrium reaction energy!", ) def test_get_quasi_e_to_hull(self): for entry in self.pd.unstable_entries: # catch duplicated stable entries if entry.normalize( inplace=False) in self.pd.get_stable_entries_normed(): self.assertLessEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Duplicated stable entries should have negative decomposition energy!", ) else: self.assertGreaterEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Unstable entries should have positive decomposition energy!", ) for entry in self.pd.stable_entries: if entry.composition.is_element: self.assertEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Stable elemental entries should have decomposition energy of zero!", ) else: self.assertLessEqual( self.pd.get_quasi_e_to_hull(entry), 0, "Stable entries should have negative decomposition energy!", ) novel_stable_entry = PDEntry("Li5FeO4", -999) self.assertLess( self.pd.get_quasi_e_to_hull(novel_stable_entry), 0, "Novel stable entries should have negative decomposition energy!", ) novel_unstable_entry = PDEntry("Li5FeO4", 999) self.assertGreater( self.pd.get_quasi_e_to_hull(novel_unstable_entry), 0, "Novel unstable entries should have positive decomposition energy!", ) duplicate_entry = PDEntry("Li2O", -14.31361175) scaled_dup_entry = PDEntry("Li4O2", -14.31361175 * 2) stable_entry = [e for e in self.pd.stable_entries if e.name == "Li2O"][0] self.assertEqual( self.pd.get_quasi_e_to_hull(duplicate_entry), self.pd.get_quasi_e_to_hull(stable_entry), "Novel duplicates of stable entries should have same decomposition energy!", ) self.assertEqual( self.pd.get_quasi_e_to_hull(scaled_dup_entry), self.pd.get_quasi_e_to_hull(stable_entry), "Novel scaled duplicates of stable entries should have same decomposition energy!", ) def test_get_decomposition(self): for entry in self.pd.stable_entries: self.assertEqual( len(self.pd.get_decomposition(entry.composition)), 1, "Stable composition should have only 1 decomposition!", ) dim = len(self.pd.elements) for entry in self.pd.all_entries: ndecomp = len(self.pd.get_decomposition(entry.composition)) self.assertTrue( ndecomp > 0 and ndecomp <= dim, "The number of decomposition phases can at most be equal to the number of components.", ) # Just to test decomp for a ficitious composition ansdict = { entry.composition.formula: amt for entry, amt in self.pd.get_decomposition( Composition("Li3Fe7O11")).items() } expected_ans = { "Fe2 O2": 0.0952380952380949, "Li1 Fe1 O2": 0.5714285714285714, "Fe6 O8": 0.33333333333333393, } for k, v in expected_ans.items(): self.assertAlmostEqual(ansdict[k], v) def test_get_transition_chempots(self): for el in self.pd.elements: self.assertLessEqual(len(self.pd.get_transition_chempots(el)), len(self.pd.facets)) def test_get_element_profile(self): for el in self.pd.elements: for entry in self.pd.stable_entries: if not (entry.composition.is_element): self.assertLessEqual( len(self.pd.get_element_profile(el, entry.composition)), len(self.pd.facets), ) expected = [ { "evolution": 1.0, "chempot": -4.2582781416666666, "reaction": "Li2O + 0.5 O2 -> Li2O2", }, { "evolution": 0, "chempot": -5.0885906699999968, "reaction": "Li2O -> Li2O", }, { "evolution": -1.0, "chempot": -10.487582010000001, "reaction": "Li2O -> 2 Li + 0.5 O2", }, ] result = self.pd.get_element_profile(Element("O"), Composition("Li2O")) for d1, d2 in zip(expected, result): self.assertAlmostEqual(d1["evolution"], d2["evolution"]) self.assertAlmostEqual(d1["chempot"], d2["chempot"]) self.assertEqual(d1["reaction"], str(d2["reaction"])) def test_get_get_chempot_range_map(self): elements = [el for el in self.pd.elements if el.symbol != "Fe"] self.assertEqual(len(self.pd.get_chempot_range_map(elements)), 10) def test_getmu_vertices_stability_phase(self): results = self.pd.getmu_vertices_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(len(results), 6) test_equality = False for c in results: if (abs(c[Element("O")] + 7.115) < 1e-2 and abs(c[Element("Fe")] + 6.596) < 1e-2 and abs(c[Element("Li")] + 3.931) < 1e-2): test_equality = True self.assertTrue(test_equality, "there is an expected vertex missing in the list") def test_getmu_range_stability_phase(self): results = self.pd.get_chempot_range_stability_phase( Composition("LiFeO2"), Element("O")) self.assertAlmostEqual(results[Element("O")][1], -4.4501812249999997) self.assertAlmostEqual(results[Element("Fe")][0], -6.5961470999999996) self.assertAlmostEqual(results[Element("Li")][0], -3.6250022625000007) def test_get_hull_energy(self): for entry in self.pd.stable_entries: h_e = self.pd.get_hull_energy(entry.composition) self.assertAlmostEqual(h_e, entry.energy) n_h_e = self.pd.get_hull_energy( entry.composition.fractional_composition) self.assertAlmostEqual(n_h_e, entry.energy_per_atom) def test_1d_pd(self): entry = PDEntry("H", 0) pd = PhaseDiagram([entry]) decomp, e = pd.get_decomp_and_e_above_hull(PDEntry("H", 1)) self.assertAlmostEqual(e, 1) self.assertAlmostEqual(decomp[entry], 1.0) def test_get_critical_compositions_fractional(self): c1 = Composition("Fe2O3").fractional_composition c2 = Composition("Li3FeO4").fractional_composition c3 = Composition("Li2O").fractional_composition comps = self.pd.get_critical_compositions(c1, c2) expected = [ Composition("Fe2O3").fractional_composition, Composition("Li0.3243244Fe0.1621621O0.51351349"), Composition("Li3FeO4").fractional_composition, ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.pd.get_critical_compositions(c1, c3) expected = [ Composition("Fe0.4O0.6"), Composition("LiFeO2").fractional_composition, Composition("Li5FeO4").fractional_composition, Composition("Li2O").fractional_composition, ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) def test_get_critical_compositions(self): c1 = Composition("Fe2O3") c2 = Composition("Li3FeO4") c3 = Composition("Li2O") comps = self.pd.get_critical_compositions(c1, c2) expected = [ Composition("Fe2O3"), Composition("Li0.3243244Fe0.1621621O0.51351349") * 7.4, Composition("Li3FeO4"), ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) comps = self.pd.get_critical_compositions(c1, c3) expected = [ Composition("Fe2O3"), Composition("LiFeO2"), Composition("Li5FeO4") / 3, Composition("Li2O"), ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # Don't fail silently if input compositions aren't in phase diagram # Can be very confusing if you're working with a GrandPotentialPD self.assertRaises( ValueError, self.pd.get_critical_compositions, Composition("Xe"), Composition("Mn"), ) # For the moment, should also fail even if compositions are in the gppd # because it isn't handled properly gppd = GrandPotentialPhaseDiagram(self.pd.all_entries, {"Xe": 1}, self.pd.elements + [Element("Xe")]) self.assertRaises( ValueError, gppd.get_critical_compositions, Composition("Fe2O3"), Composition("Li3FeO4Xe"), ) # check that the function still works though comps = gppd.get_critical_compositions(c1, c2) expected = [ Composition("Fe2O3"), Composition("Li0.3243244Fe0.1621621O0.51351349") * 7.4, Composition("Li3FeO4"), ] for crit, exp in zip(comps, expected): self.assertTrue(crit.almost_equals(exp, rtol=0, atol=1e-5)) # case where the endpoints are identical self.assertEqual(self.pd.get_critical_compositions(c1, c1 * 2), [c1, c1 * 2]) def test_get_composition_chempots(self): c1 = Composition("Fe3.1O4") c2 = Composition("Fe3.2O4.1Li0.01") e1 = self.pd.get_hull_energy(c1) e2 = self.pd.get_hull_energy(c2) cp = self.pd.get_composition_chempots(c1) calc_e2 = e1 + sum(cp[k] * v for k, v in (c2 - c1).items()) self.assertAlmostEqual(e2, calc_e2) def test_get_all_chempots(self): c1 = Composition("Fe3.1O4") c2 = Composition("FeO") cp1 = self.pd.get_all_chempots(c1) cpresult = { Element("Li"): -4.077061954999998, Element("Fe"): -6.741593864999999, Element("O"): -6.969907375000003, } for elem, energy in cpresult.items(): self.assertAlmostEqual(cp1["Fe3O4-FeO-LiFeO2"][elem], energy) cp2 = self.pd.get_all_chempots(c2) cpresult = { Element("O"): -7.115354140000001, Element("Fe"): -6.5961471, Element("Li"): -3.9316151899999987, } for elem, energy in cpresult.items(): self.assertAlmostEqual(cp2["FeO-LiFeO2-Fe"][elem], energy) def test_to_from_dict(self): # test round-trip for other entry types such as ComputedEntry entry = ComputedEntry("H", 0.0, 0.0, entry_id="test") pd = PhaseDiagram([entry]) d = pd.as_dict() pd_roundtrip = PhaseDiagram.from_dict(d) self.assertEqual(pd.all_entries[0].entry_id, pd_roundtrip.all_entries[0].entry_id)
# Generate a phase diagram to consider only solid entries stable in water. pd = PhaseDiagram(entries_aqcorr) stable_solids = pd.stable_entries stable_solids_minus_h2o = [ entry for entry in stable_solids if entry.composition.reduced_formula not in ["H2", "O2", "H2O", "H2O2"] ] # + pbx_solid_entries = [] for entry in stable_solids_minus_h2o: pbx_entry = PourbaixEntry(entry) # Replace E with newly corrected E pbx_entry.g0_replace(pd.get_form_energy(entry)) pbx_entry.reduced_entry() # Applies reduction factor????? pbx_solid_entries.append(pbx_entry) # | - Processing My Solid Entries for key, value in ion_dict_solids_expt.items(): print("IDJFIJDS") split_key = key.split("_") formula_i = split_key[0] comp = Ion.from_formula(formula_i) energy = value pbx_entry_ion = PourbaixEntry( ComputedEntry(comp, energy, attribute={"full_name": key})) # AP pbx_entry_ion.name = key
def get_pourbaix_entries(self, chemsys): """ A helper function to get all entries necessary to generate a pourbaix diagram from the rest interface. Args: chemsys ([str]): A list of elements comprising the chemical system, e.g. ['Li', 'Fe'] """ from pymatgen.analysis.pourbaix_diagram import PourbaixEntry, IonEntry from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.core.ion import Ion from pymatgen.entries.compatibility import\ MaterialsProjectAqueousCompatibility pbx_entries = [] # Get ion entries first, because certain ions have reference # solids that aren't necessarily in the chemsys (Na2SO4) url = '/pourbaix_diagram/reference_data/' + '-'.join(chemsys) ion_data = self._make_request(url) ion_ref_comps = [Composition(d['Reference Solid']) for d in ion_data] ion_ref_elts = list(itertools.chain.from_iterable( i.elements for i in ion_ref_comps)) ion_ref_entries = self.get_entries_in_chemsys( list(set([str(e) for e in ion_ref_elts] + ['O', 'H'])), property_data=['e_above_hull'], compatible_only=False) compat = MaterialsProjectAqueousCompatibility("Advanced") ion_ref_entries = compat.process_entries(ion_ref_entries) ion_ref_pd = PhaseDiagram(ion_ref_entries) # position the ion energies relative to most stable reference state for n, i_d in enumerate(ion_data): ion_entry = IonEntry(Ion.from_formula(i_d['Name']), i_d['Energy']) refs = [e for e in ion_ref_entries if e.composition.reduced_formula == i_d['Reference Solid']] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.data['e_above_hull'])[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] solid_diff = ion_ref_pd.get_form_energy(stable_ref)\ - i_d['Reference solid energy'] * rf elt = i_d['Major_Elements'][0] correction_factor = ion_entry.ion.composition[elt]\ / stable_ref.composition[elt] ion_entry.energy += solid_diff * correction_factor pbx_entries.append(PourbaixEntry(ion_entry, 'ion-{}'.format(n))) # import nose; nose.tools.set_trace() # Construct the solid pourbaix entries from filtered ion_ref entries extra_elts = set(ion_ref_elts) - {Element(s) for s in chemsys}\ - {Element('H'), Element('O')} for entry in ion_ref_entries: entry_elts = set(entry.composition.elements) # Ensure no OH chemsys or extraneous elements from ion references if not (entry_elts <= {Element('H'), Element('O')} or \ extra_elts.intersection(entry_elts)): # replace energy with formation energy, use dict to # avoid messing with the ion_ref_pd and to keep all old params form_e = ion_ref_pd.get_form_energy(entry) new_entry = deepcopy(entry) new_entry.uncorrected_energy = form_e new_entry.correction = 0.0 pbx_entry = PourbaixEntry(new_entry) if entry.entry_id == "mp-697146": pass # import nose; nose.tools.set_trace() # pbx_entry.reduced_entry() pbx_entries.append(pbx_entry) return pbx_entries
def get_ion_entries(self, pd: PhaseDiagram, ion_ref_data: List[dict] = None) -> List[IonEntry]: """ Retrieve IonEntry objects that can be used in the construction of Pourbaix Diagrams. The energies of the IonEntry are calculaterd from the solid energies in the provided Phase Diagram to be consistent with experimental free energies. NOTE! This is an advanced method that assumes detailed understanding of how to construct computational Pourbaix Diagrams. If you just want to build a Pourbaix Diagram using default settings, use get_pourbaix_entries. Args: pd: Solid phase diagram on which to construct IonEntry. Note that this Phase Diagram MUST include O and H in its chemical system. For example, to retrieve IonEntry for Ti, the phase diagram passed here should contain materials in the H-O-Ti chemical system. It is also assumed that solid energies have already been corrected with MaterialsProjectAqueousCompatibility, which is necessary for proper construction of Pourbaix diagrams. ion_ref_data: Aqueous ion reference data. If None (default), the data are downloaded from the Aqueous Ion Reference Data project hosted on MPContribs. To add a custom ionic species, first download data using get_ion_reference_data, then add or customize it with your additional data, and pass the customized list here. Returns: [IonEntry]: IonEntry are similar to PDEntry objects. Their energies are free energies in eV. """ # determine the chemsys from the phase diagram chemsys = "-".join([el.symbol for el in pd.elements]) # raise ValueError if O and H not in chemsys if "O" not in chemsys or "H" not in chemsys: raise ValueError( "The phase diagram chemical system must contain O and H! Your" f" diagram chemical system is {chemsys}.") if not ion_ref_data: ion_data = self.get_ion_reference_data_for_chemsys(chemsys) else: ion_data = ion_ref_data # position the ion energies relative to most stable reference state ion_entries = [] for n, i_d in enumerate(ion_data): ion = Ion.from_formula(i_d["formula"]) refs = [ e for e in pd.all_entries if e.composition.reduced_formula == i_d["data"]["RefSolid"] ] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.energy_per_atom)[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] # TODO - need a more robust way to convert units # use pint here? if i_d["data"]["ΔGᶠRefSolid"]["unit"] == "kJ/mol": # convert to eV/formula unit ref_solid_energy = i_d["data"]["ΔGᶠRefSolid"]["value"] / 96.485 elif i_d["data"]["ΔGᶠRefSolid"]["unit"] == "MJ/mol": # convert to eV/formula unit ref_solid_energy = i_d["data"]["ΔGᶠRefSolid"]["value"] / 96485 else: raise ValueError( f"Ion reference solid energy has incorrect unit {i_d['data']['ΔGᶠRefSolid']['unit']}" ) solid_diff = pd.get_form_energy(stable_ref) - ref_solid_energy * rf elt = i_d["data"]["MajElements"] correction_factor = ion.composition[elt] / stable_ref.composition[ elt] # TODO - need a more robust way to convert units # use pint here? if i_d["data"]["ΔGᶠ"]["unit"] == "kJ/mol": # convert to eV/formula unit ion_free_energy = i_d["data"]["ΔGᶠ"]["value"] / 96.485 elif i_d["data"]["ΔGᶠ"]["unit"] == "MJ/mol": # convert to eV/formula unit ion_free_energy = i_d["data"]["ΔGᶠ"]["value"] / 96485 else: raise ValueError( f"Ion free energy has incorrect unit {i_d['data']['ΔGᶠ']['unit']}" ) energy = ion_free_energy + solid_diff * correction_factor ion_entries.append(IonEntry(ion, energy)) return ion_entries
def get_pourbaix_entries( self, chemsys: Union[str, List], solid_compat="MaterialsProject2020Compatibility", use_gibbs: Optional[Literal[300]] = None, ): """ A helper function to get all entries necessary to generate a Pourbaix diagram from the rest interface. Args: chemsys (str or [str]): Chemical system string comprising element symbols separated by dashes, e.g., "Li-Fe-O" or List of element symbols, e.g., ["Li", "Fe", "O"]. solid_compat: Compatiblity scheme used to pre-process solid DFT energies prior to applying aqueous energy adjustments. May be passed as a class (e.g. MaterialsProject2020Compatibility) or an instance (e.g., MaterialsProject2020Compatibility()). If None, solid DFT energies are used as-is. Default: MaterialsProject2020Compatibility use_gibbs: Set to 300 (for 300 Kelvin) to use a machine learning model to estimate solid free energy from DFT energy (see GibbsComputedStructureEntry). This can slightly improve the accuracy of the Pourbaix diagram in some cases. Default: None. Note that temperatures other than 300K are not permitted here, because MaterialsProjectAqueousCompatibility corrections, used in Pourbaix diagram construction, are calculated based on 300 K data. """ # imports are not top-level due to expense from pymatgen.analysis.pourbaix_diagram import PourbaixEntry from pymatgen.entries.compatibility import ( Compatibility, MaterialsProject2020Compatibility, MaterialsProjectAqueousCompatibility, MaterialsProjectCompatibility, ) from pymatgen.entries.computed_entries import ComputedEntry if solid_compat == "MaterialsProjectCompatibility": solid_compat = MaterialsProjectCompatibility() elif solid_compat == "MaterialsProject2020Compatibility": solid_compat = MaterialsProject2020Compatibility() elif isinstance(solid_compat, Compatibility): solid_compat = solid_compat else: raise ValueError( "Solid compatibility can only be 'MaterialsProjectCompatibility', " "'MaterialsProject2020Compatibility', or an instance of a Compatability class" ) pbx_entries = [] if isinstance(chemsys, str): chemsys = chemsys.split("-") # capitalize and sort the elements chemsys = sorted(e.capitalize() for e in chemsys) # Get ion entries first, because certain ions have reference # solids that aren't necessarily in the chemsys (Na2SO4) # download the ion reference data from MPContribs ion_data = self.get_ion_reference_data_for_chemsys(chemsys) # build the PhaseDiagram for get_ion_entries ion_ref_comps = [ Ion.from_formula(d["data"]["RefSolid"]).composition for d in ion_data ] ion_ref_elts = set( itertools.chain.from_iterable(i.elements for i in ion_ref_comps)) # TODO - would be great if the commented line below would work # However for some reason you cannot process GibbsComputedStructureEntry with # MaterialsProjectAqueousCompatibility ion_ref_entries = self.get_entries_in_chemsys( list([str(e) for e in ion_ref_elts] + ["O", "H"]), # use_gibbs=use_gibbs ) # suppress the warning about supplying the required energies; they will be calculated from the # entries we get from MPRester with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="You did not provide the required O2 and H2O energies.", ) compat = MaterialsProjectAqueousCompatibility( solid_compat=solid_compat) # suppress the warning about missing oxidation states with warnings.catch_warnings(): warnings.filterwarnings( "ignore", message="Failed to guess oxidation states.*") ion_ref_entries = compat.process_entries(ion_ref_entries) # TODO - if the commented line above would work, this conditional block # could be removed if use_gibbs: # replace the entries with GibbsComputedStructureEntry from pymatgen.entries.computed_entries import GibbsComputedStructureEntry ion_ref_entries = GibbsComputedStructureEntry.from_entries( ion_ref_entries, temp=use_gibbs) ion_ref_pd = PhaseDiagram(ion_ref_entries) ion_entries = self.get_ion_entries(ion_ref_pd, ion_ref_data=ion_data) pbx_entries = [ PourbaixEntry(e, f"ion-{n}") for n, e in enumerate(ion_entries) ] # Construct the solid pourbaix entries from filtered ion_ref entries extra_elts = (set(ion_ref_elts) - {Element(s) for s in chemsys} - {Element("H"), Element("O")}) for entry in ion_ref_entries: entry_elts = set(entry.composition.elements) # Ensure no OH chemsys or extraneous elements from ion references if not (entry_elts <= {Element("H"), Element("O")} or extra_elts.intersection(entry_elts)): # Create new computed entry form_e = ion_ref_pd.get_form_energy(entry) new_entry = ComputedEntry(entry.composition, form_e, entry_id=entry.entry_id) pbx_entry = PourbaixEntry(new_entry) pbx_entries.append(pbx_entry) return pbx_entries
if el_name != 'O': chem_potentials[corner][el] += (-1) * (computed_phases[el_name]) else: chem_potentials[corner][el] += (-1) * (computed_phases['O2'] / 2) # #print('NN_HSE_aexx25') #for zone in chem_potentials: # # print('') # print(zone) # print(chem_potentials[zone]) NN_entry = entries[0] NN_form_energy = pd.get_form_energy(NN_entry) def chem_pot_O_fixed(mu_el, NN_form_energy, mu_O=-1.09): mu_variable = NN_form_energy - mu_el - mu_O return mu_variable mu_Na_min, mu_Na_max = pd.get_chempot_range_stability_phase( comp, Element('Na'))[Element('Na')] mu_Nb_min, mu_Nb_max = pd.get_chempot_range_stability_phase( comp, Element('Na'))[Element('Nb')] mu_Nb_min += -1 * computed_phases['Nb'] mu_Nb_max += -1 * computed_phases['Nb']
def get_pourbaix_entries(self, chemsys): """ A helper function to get all entries necessary to generate a pourbaix diagram from the rest interface. Args: chemsys ([str]): A list of elements comprising the chemical system, e.g. ['Li', 'Fe'] """ from pymatgen.analysis.pourbaix_diagram import PourbaixEntry, IonEntry from pymatgen.analysis.phase_diagram import PhaseDiagram from pymatgen.core.ion import Ion from pymatgen.entries.compatibility import\ MaterialsProjectAqueousCompatibility pbx_entries = [] # Get ion entries first, because certain ions have reference # solids that aren't necessarily in the chemsys (Na2SO4) url = '/pourbaix_diagram/reference_data/' + '-'.join(chemsys) ion_data = self._make_request(url) ion_ref_comps = [Composition(d['Reference Solid']) for d in ion_data] ion_ref_elts = list(itertools.chain.from_iterable( i.elements for i in ion_ref_comps)) ion_ref_entries = self.get_entries_in_chemsys( list(set([str(e) for e in ion_ref_elts] + ['O', 'H'])), property_data=['e_above_hull'], compatible_only=False) compat = MaterialsProjectAqueousCompatibility("Advanced") ion_ref_entries = compat.process_entries(ion_ref_entries) ion_ref_pd = PhaseDiagram(ion_ref_entries) # position the ion energies relative to most stable reference state for n, i_d in enumerate(ion_data): ion_entry = IonEntry(Ion.from_formula(i_d['Name']), i_d['Energy']) refs = [e for e in ion_ref_entries if e.composition.reduced_formula == i_d['Reference Solid']] if not refs: raise ValueError("Reference solid not contained in entry list") stable_ref = sorted(refs, key=lambda x: x.data['e_above_hull'])[0] rf = stable_ref.composition.get_reduced_composition_and_factor()[1] solid_diff = ion_ref_pd.get_form_energy(stable_ref)\ - i_d['Reference solid energy'] * rf elt = i_d['Major_Elements'][0] correction_factor = ion_entry.ion.composition[elt]\ / stable_ref.composition[elt] ion_entry.energy += solid_diff * correction_factor pbx_entries.append(PourbaixEntry(ion_entry, 'ion-{}'.format(n))) # import nose; nose.tools.set_trace() # Construct the solid pourbaix entries from filtered ion_ref entries extra_elts = set(ion_ref_elts) - {Element(s) for s in chemsys}\ - {Element('H'), Element('O')} for entry in ion_ref_entries: entry_elts = set(entry.composition.elements) # Ensure no OH chemsys or extraneous elements from ion references if not (entry_elts <= {Element('H'), Element('O')} or \ extra_elts.intersection(entry_elts)): # replace energy with formation energy, use dict to # avoid messing with the ion_ref_pd and to keep all old params form_e = ion_ref_pd.get_form_energy(entry) new_entry = deepcopy(entry) new_entry.uncorrected_energy = form_e new_entry.correction = 0.0 pbx_entry = PourbaixEntry(new_entry) if entry.entry_id == "mp-697146": pass # import nose; nose.tools.set_trace() # pbx_entry.reduced_entry() pbx_entries.append(pbx_entry) return pbx_entries