def setUp(self): entrylist = list() weights = list() comp = Composition("Mn2O3") entry = PDEntry(comp, 49) entrylist.append(PourbaixEntry(entry)) weights.append(1.0) comp = Ion.from_formula("MnO4[-]") entry = IonEntry(comp, 25) entrylist.append(PourbaixEntry(entry)) weights.append(0.25) comp = Composition("Fe2O3") entry = PDEntry(comp, 50) entrylist.append(PourbaixEntry(entry)) weights.append(0.5) comp = Ion.from_formula("Fe[2+]") entry = IonEntry(comp, 15) entrylist.append(PourbaixEntry(entry)) weights.append(2.5) comp = Ion.from_formula("Fe[3+]") entry = IonEntry(comp, 20) entrylist.append(PourbaixEntry(entry)) weights.append(1.5) self.weights = weights self.entrylist = entrylist self.multientry = MultiEntry(entrylist, weights)
def setUp(self): self.comp = list() self.comp.append(Ion.from_formula("Li+")) self.comp.append(Ion.from_formula("MnO4-")) self.comp.append(Ion.from_formula("Mn++")) self.comp.append(Ion.from_formula("PO3-2")) self.comp.append(Ion.from_formula("Fe(CN)6-3")) self.comp.append(Ion.from_formula("Fe(CN)6----")) self.comp.append(Ion.from_formula("Fe2((PO4)3(CO3)5)2-3")) self.comp.append(Ion.from_formula("Ca[2+]")) self.comp.append(Ion.from_formula("NaOH(aq)"))
def len_elts(entry): if "(s)" in entry: comp = Composition(entry[:-3]) else: comp = Ion.from_formula(entry) return len([el for el in comp.elements if el not in [Element("H"), Element("O")]])
def test_mpr_pipeline(self): from pymatgen.ext.matproj import MPRester mpr = MPRester() data = mpr.get_pourbaix_entries(["Zn"]) pbx = PourbaixDiagram(data, filter_solids=True, conc_dict={"Zn": 1e-8}) pbx.find_stable_entry(10, 0) data = mpr.get_pourbaix_entries(["Ag", "Te"]) pbx = PourbaixDiagram(data, filter_solids=True, conc_dict={"Ag": 1e-8, "Te": 1e-8}) self.assertEqual(len(pbx.stable_entries), 30) test_entry = pbx.find_stable_entry(8, 2) self.assertAlmostEqual(test_entry.energy, 2.3894017960000009, 1) # Test custom ions entries = mpr.get_pourbaix_entries(["Sn", "C", "Na"]) ion = IonEntry(Ion.from_formula("NaO28H80Sn12C24+"), -161.676) custom_ion_entry = PourbaixEntry(ion, entry_id="my_ion") pbx = PourbaixDiagram( entries + [custom_ion_entry], filter_solids=True, comp_dict={"Na": 1, "Sn": 12, "C": 24}, ) self.assertAlmostEqual(pbx.get_decomposition_energy(custom_ion_entry, 5, 2), 2.1209002582, 1) # Test against ion sets with multiple equivalent ions (Bi-V regression) entries = mpr.get_pourbaix_entries(["Bi", "V"]) pbx = PourbaixDiagram(entries, filter_solids=True, conc_dict={"Bi": 1e-8, "V": 1e-8}) self.assertTrue(all(["Bi" in entry.composition and "V" in entry.composition for entry in pbx.all_entries]))
def test_mpr_pipeline(self): from pymatgen import MPRester mpr = MPRester() data = mpr.get_pourbaix_entries(["Zn"]) pbx = PourbaixDiagram(data, filter_solids=True, conc_dict={"Zn": 1e-8}) pbx.find_stable_entry(10, 0) data = mpr.get_pourbaix_entries(["Ag", "Te"]) pbx = PourbaixDiagram(data, filter_solids=True, conc_dict={ "Ag": 1e-8, "Te": 1e-8 }) self.assertEqual(len(pbx.stable_entries), 30) test_entry = pbx.find_stable_entry(8, 2) self.assertAlmostEqual(test_entry.energy, 2.3936747835000016, 3) # Test custom ions entries = mpr.get_pourbaix_entries(["Sn", "C", "Na"]) ion = IonEntry(Ion.from_formula("NaO28H80Sn12C24+"), -161.676) custom_ion_entry = PourbaixEntry(ion, entry_id='my_ion') pbx = PourbaixDiagram(entries + [custom_ion_entry], filter_solids=True, comp_dict={ "Na": 1, "Sn": 12, "C": 24 }) self.assertAlmostEqual( pbx.get_decomposition_energy(custom_ion_entry, 5, 2), 8.31202738629504, 2)
def test_read_write_csv(self): Zn_solids = ["Zn", "ZnO", "ZnO2"] sol_g = [0.0, -3.338, -1.315] Zn_ions = ["Zn[2+]", "ZnOH[+]", "HZnO2[-]", "ZnO2[2-]", "ZnO"] liq_g = [-1.527, -3.415, -4.812, -4.036, -2.921] liq_conc = [1e-6, 1e-6, 1e-6, 1e-6, 1e-6] solid_entry = list() for sol in Zn_solids: comp = Composition(sol) delg = sol_g[Zn_solids.index(sol)] solid_entry.append(PourbaixEntry(PDEntry(comp, delg))) ion_entry = list() for ion in Zn_ions: comp_ion = Ion.from_formula(ion) delg = liq_g[Zn_ions.index(ion)] conc = liq_conc[Zn_ions.index(ion)] PoE = PourbaixEntry(IonEntry(comp_ion, delg)) PoE.conc = conc ion_entry.append(PoE) entries = solid_entry + ion_entry PourbaixEntryIO.to_csv("pourbaix_test_entries.csv", entries) (elements, entries) = PourbaixEntryIO.from_csv("pourbaix_test_entries.csv") self.assertEqual( elements, [Element('Zn'), Element('H'), Element('O')], "Wrong elements!") self.assertEqual(len(entries), 8, "Wrong number of entries!") os.remove("pourbaix_test_entries.csv")
def test_read_write_csv(self): Zn_solids = ["Zn", "ZnO", "ZnO2"] sol_g = [0.0, -3.338, -1.315] Zn_ions = ["Zn[2+]", "ZnOH[+]", "HZnO2[-]", "ZnO2[2-]", "ZnO"] liq_g = [-1.527, -3.415, -4.812, -4.036, -2.921] liq_conc = [1e-6, 1e-6, 1e-6, 1e-6, 1e-6] solid_entry = list() for sol in Zn_solids: comp = Composition(sol) delg = sol_g[Zn_solids.index(sol)] solid_entry.append(PourbaixEntry(PDEntry(comp, delg))) ion_entry = list() for ion in Zn_ions: comp_ion = Ion.from_formula(ion) delg = liq_g[Zn_ions.index(ion)] conc = liq_conc[Zn_ions.index(ion)] PoE = PourbaixEntry(IonEntry(comp_ion, delg)) PoE.set_conc(conc) ion_entry.append(PoE) entries = solid_entry + ion_entry PourbaixEntryIO.to_csv("pourbaix_test_entries.csv", entries) (elements, entries) = PourbaixEntryIO.from_csv( "pourbaix_test_entries.csv") self.assertEqual(elements, [Element('Zn'), Element('H'), Element('O')], "Wrong elements!") self.assertEqual(len(entries), 8, "Wrong number of entries!") os.remove("pourbaix_test_entries.csv")
def setUp(self): comp = Composition("Mn2O3") self.solentry = PDEntry(comp, 49) ion = Ion.from_formula("MnO4-") self.ionentry = IonEntry(ion, 25) self.PxIon = PourbaixEntry(self.ionentry) self.PxSol = PourbaixEntry(self.solentry) self.PxIon.set_conc(1e-4)
def setUp(self): comp = Composition("Mn2O3") self.solentry = PDEntry(comp, 49) ion = Ion.from_formula("MnO4-") self.ionentry = IonEntry(ion, 25) self.PxIon = PourbaixEntry(self.ionentry) self.PxSol = PourbaixEntry(self.solentry) self.PxIon.conc = 1e-4
def setUp(self): # comp = Composition("Mn2O3") self.solentry = ComputedEntry("Mn2O3", 49) ion = Ion.from_formula("MnO4-") self.ionentry = IonEntry(ion, 25) self.PxIon = PourbaixEntry(self.ionentry) self.PxSol = PourbaixEntry(self.solentry) self.PxIon.concentration = 1e-4
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)
def test_special_formulas(self): special_formulas = [ ("Cl-", "Cl[-1]"), ("H+", "H[+1]"), ("F-", "F[-1]"), ("H4O4", "H2O2(aq)"), ("OH-", "OH[-1]"), ("CH3COO-", "CH3COO[-1]"), ("CH3COOH", "CH3COOH(aq)"), ("CH3OH", "CH3OH(aq)"), ("H4CO", "CH3OH(aq)"), ("C2H6O", "C2H5OH(aq)"), ("C3H8O", "C3H7OH(aq)"), ("C4H10O", "C4H9OH(aq)"), ("Fe(OH)4+", "FeO2.2H2O[+1]"), ("Zr(OH)4", "ZrO2.2H2O(aq)"), ] for tup in special_formulas: self.assertEqual(Ion.from_formula(tup[0]).reduced_formula, tup[1]) self.assertEqual(Ion.from_formula("Fe(OH)4+").get_reduced_formula_and_factor(hydrates=False), ("Fe(OH)4", 1)) self.assertEqual(Ion.from_formula("Zr(OH)4").get_reduced_formula_and_factor(hydrates=False), ("Zr(OH)4", 1))
def __init__(self, entries, comp_dict=None): """ Args: entries: Entries list containing both Solids and Ions comp_dict: Dictionary of compositions """ self._solid_entries = list() self._ion_entries = list() for entry in entries: if entry.phase_type == "Solid": self._solid_entries.append(entry) elif entry.phase_type == "Ion": self._ion_entries.append(entry) else: raise StandardError("Incorrect Phase type - needs to be \ Pourbaix entry of phase type Ion/Solid") self._unprocessed_entries = self._solid_entries + self._ion_entries self._elt_comp = comp_dict if comp_dict: self._multielement = True self.pourbaix_elements = [key for key in comp_dict] w = [comp_dict[key] for key in comp_dict] A = [] for comp in comp_dict: m = re.search(r"\[([^\[\]]+)\]|\(aq\)", comp) if m: comp_obj = Ion.from_formula(comp) else: comp_obj = Composition.from_formula(comp) Ai = [] for elt in self.pourbaix_elements: Ai.append(comp_obj[Element(elt)]) A.append(Ai) A = np.array(A).T.astype(float) w = np.array(w) A /= np.dot([A[i].sum() for i in xrange(len(A))], w) x = np.linalg.solve(A, w) self._elt_comp = dict(zip(self.pourbaix_elements, x)) else: self._multielement = False self.pourbaix_elements = [ el.symbol for el in entries[0].composition.elements if el.symbol not in ["H", "O"] ] self._make_pourbaixdiagram()
def __init__(self, entries, comp_dict=None): """ Args: entries: Entries list containing both Solids and Ions comp_dict: Dictionary of compositions """ self._solid_entries = list() self._ion_entries = list() for entry in entries: if entry.phase_type == "Solid": self._solid_entries.append(entry) elif entry.phase_type == "Ion": self._ion_entries.append(entry) else: raise StandardError("Incorrect Phase type - needs to be \ Pourbaix entry of phase type Ion/Solid") self._unprocessed_entries = self._solid_entries + self._ion_entries self._elt_comp = comp_dict if comp_dict: self._multielement = True self.pourbaix_elements = [key for key in comp_dict] w = [comp_dict[key] for key in comp_dict] A = [] for comp in comp_dict: m = re.search(r"\[([^\[\]]+)\]|\(aq\)", comp) if m: comp_obj = Ion.from_formula(comp) else: comp_obj = Composition.from_formula(comp) Ai = [] for elt in self.pourbaix_elements: Ai.append(comp_obj[Element(elt)]) A.append(Ai) A = np.array(A).T.astype(float) w = np.array(w) A /= np.dot([A[i].sum() for i in xrange(len(A))], w) x = np.linalg.solve(A, w) self._elt_comp = dict(zip(self.pourbaix_elements, x)) else: self._multielement = False self.pourbaix_elements = [el.symbol for el in entries[0].composition.elements if el.symbol not in ["H", "O"]] self._make_pourbaixdiagram()
def test_get_decomposition(self): # Test a stable entry to ensure that it's zero in the stable region entry = self.test_data["Zn"][12] # Should correspond to mp-2133 self.assertAlmostEqual( self.pbx.get_decomposition_energy(entry, 10, 1), 0.0, 5, "Decomposition energy of ZnO is not 0.", ) # Test an unstable entry to ensure that it's never zero entry = self.test_data["Zn"][11] ph, v = np.meshgrid(np.linspace(0, 14), np.linspace(-2, 4)) result = self.pbx_nofilter.get_decomposition_energy(entry, ph, v) self.assertTrue((result >= 0).all(), "Unstable energy has hull energy of 0 or less") # Test an unstable hydride to ensure HER correction works self.assertAlmostEqual( self.pbx.get_decomposition_energy(entry, -3, -2), 3.6979147983333) # Test a list of pHs self.pbx.get_decomposition_energy(entry, np.linspace(0, 2, 5), 2) # Test a list of Vs self.pbx.get_decomposition_energy(entry, 4, np.linspace(-3, 3, 10)) # Test a set of matching arrays ph, v = np.meshgrid(np.linspace(0, 14), np.linspace(-3, 3)) self.pbx.get_decomposition_energy(entry, ph, v) # Test custom ions entries = self.test_data["C-Na-Sn"] ion = IonEntry(Ion.from_formula("NaO28H80Sn12C24+"), -161.676) custom_ion_entry = PourbaixEntry(ion, entry_id="my_ion") pbx = PourbaixDiagram( entries + [custom_ion_entry], filter_solids=True, comp_dict={ "Na": 1, "Sn": 12, "C": 24 }, ) self.assertAlmostEqual( pbx.get_decomposition_energy(custom_ion_entry, 5, 2), 2.1209002582, 1)
def ion_or_solid_comp_object(formula): """ Returns either an ion object or composition object given a formula. Args: formula: String formula. Eg. of ion: NaOH(aq), Na[+]; Eg. of solid: Fe2O3(s), Fe(s), Na2O Returns: Composition/Ion object """ m = re.search(r"\[([^\[\]]+)\]|\(aq\)", formula) if m: comp_obj = Ion.from_formula(formula) elif re.search(r"\(s\)", formula): comp_obj = Composition(formula[:-3]) else: comp_obj = Composition(formula) return comp_obj
def from_csv(filename): """ Imports PourbaixEntries from a csv. Args: filename - Filename to import from. Returns: List of Entries """ import csv reader = csv.reader(open(filename, "rb"), delimiter=",", quotechar="\"", quoting=csv.QUOTE_MINIMAL) entries = list() header_read = False for row in reader: if not header_read: elements = row[1:(len(row) - 4)] header_read = True else: name = row[0] energy = float(row[-4]) conc = float(row[-1]) comp = dict() for ind in range(1, len(row) - 4): if float(row[ind]) > 0: comp[Element(elements[ind - 1])] = float(row[ind]) phase_type = row[-3] if phase_type == "Ion": PoE = PourbaixEntry( IonEntry(Ion.from_formula(name), energy)) PoE.set_conc(conc) PoE.set_name(name) entries.append(PoE) else: entries.append( PourbaixEntry(PDEntry(Composition(comp), energy))) elements = [Element(el) for el in elements] return elements, entries
def from_csv(filename): """ Imports PourbaixEntries from a csv. Args: filename: Filename to import from. Returns: List of Entries """ with open(filename, "r") as f: reader = csv.reader(f, delimiter=unicode2str(","), quotechar=unicode2str("\""), quoting=csv.QUOTE_MINIMAL) entries = list() header_read = False for row in reader: if not header_read: elements = row[1:(len(row) - 4)] header_read = True else: name = row[0] energy = float(row[-4]) conc = float(row[-1]) comp = dict() for ind in range(1, len(row) - 4): if float(row[ind]) > 0: comp[Element(elements[ind - 1])] = float(row[ind]) phase_type = row[-3] if phase_type == "Ion": PoE = PourbaixEntry(IonEntry(Ion.from_formula(name), energy)) PoE.conc = conc PoE.name = name entries.append(PoE) else: entries.append(PourbaixEntry(PDEntry(Composition(comp), energy))) elements = [Element(el) for el in elements] return elements, entries
def test_mpr_pipeline(self): from pymatgen import MPRester mpr = MPRester() data = mpr.get_pourbaix_entries(["Zn"]) pbx = PourbaixDiagram(data, filter_solids=True, conc_dict={"Zn": 1e-8}) pbx.find_stable_entry(10, 0) data = mpr.get_pourbaix_entries(["Ag", "Te"]) pbx = PourbaixDiagram(data, filter_solids=True, conc_dict={"Ag": 1e-8, "Te": 1e-8}) self.assertEqual(len(pbx.stable_entries), 30) test_entry = pbx.find_stable_entry(8, 2) self.assertAlmostEqual(test_entry.energy, 2.393900378500001) # Test custom ions entries = mpr.get_pourbaix_entries(["Sn", "C", "Na"]) ion = IonEntry(Ion.from_formula("NaO28H80Sn12C24+"), -161.676) custom_ion_entry = PourbaixEntry(ion, entry_id='my_ion') pbx = PourbaixDiagram(entries + [custom_ion_entry], filter_solids=True, comp_dict={"Na": 1, "Sn": 12, "C": 24}) self.assertAlmostEqual(pbx.get_decomposition_energy(custom_ion_entry, 5, 2), 8.31082110278154)
def mke_pour_ion_entr(mtnme, ion_dict, stable_solids_minus_h2o, ref_state, entries_aqcorr, ref_dict): """ """ #| - mke_pour_ion_entr from pymatgen import Element # Accesses properties of element from pymatgen.core.ion import Ion from pymatgen.phasediagram.maker import PhaseDiagram from pymatgen.analysis.pourbaix.entry import PourbaixEntry, IonEntry from pd_make import ref_entry_find, ref_entry_stoich pd = PhaseDiagram(entries_aqcorr) ref_entry = ref_entry_find(stable_solids_minus_h2o, ref_state) ref_stoich_fact = ref_entry_stoich(ref_entry) ## Calculate DFT reference E for ions (Persson et al, PRB (2012)) dft_for_e=pd.get_form_energy(ref_entry)/ref_stoich_fact # DFT formation E, normalized by composition "factor" ion_correction_1 = dft_for_e-ref_dict[ref_state] # Difference of DFT form E and exp for E of reference el = Element(mtnme) pbx_ion_entries_1 = [] for id in ion_dict: comp = Ion.from_formula(id['Name']) # Ion name-> Ion comp name (ex. Fe[3+] -> Ion: Fe1 +3) # comp.composition[el] : number of Fe atoms in ion num_el_ref = (ref_entry.composition[el]) / ref_stoich_fact # number of element atoms in reference factor = comp.composition[el] / num_el_ref # Stoicheometric factor for ionic correction # (i.e. Fe2O3 ref but calc E for Fe[2+] ion) energy = id['Energy'] + ion_correction_1 * factor #TEMP_PRINT if id['Name']=='Pd[2+]': energy = 123 # print id['Name'] pbx_entry_ion = PourbaixEntry(IonEntry(comp, energy)) pbx_entry_ion.name = id['Name'] pbx_ion_entries_1.append(pbx_entry_ion) return pbx_ion_entries_1
Created on Wed Feb 28 15:40:02 2018 @author: jherfson """ from pymatgen.analysis.pourbaix.entry import PourbaixEntry, IonEntry #, MultiEntry from pymatgen.analysis.pourbaix.entry import PourbaixEntryIO from pymatgen.analysis.phase_diagram import PDEntry from pymatgen.core.ion import Ion from pymatgen.core.structure import Composition Zn_solids = ["Zn", "ZnO", "ZnO2"] sol_g = [0.0, -3.338, -1.315] Zn_ions = ["Zn[2+]", "ZnOH[+]", "HZnO2[-]", "ZnO2[2-]", "ZnO"] liq_g = [-1.527, -3.415, -4.812, -4.036, -2.921] liq_conc = [1e-6, 1e-6, 1e-6, 1e-6, 1e-6] solid_entry = list() for sol in Zn_solids: comp = Composition(sol) delg = sol_g[Zn_solids.index(sol)] solid_entry.append(PourbaixEntry(PDEntry(comp, delg))) ion_entry = list() for ion in Zn_ions: comp_ion = Ion.from_formula(ion) delg = liq_g[Zn_ions.index(ion)] conc = liq_conc[Zn_ions.index(ion)] PoE = PourbaixEntry(IonEntry(comp_ion, delg)) PoE.conc = conc ion_entry.append(PoE) entries = solid_entry + ion_entry PourbaixEntryIO.to_csv("pourbaix_test_entries.csv", 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_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_pourbaix_plot_colorfill_by_element(self, limits=None, title="", label_domains=True, element=None): """ Color domains by element """ from matplotlib.patches import Polygon entry_dict_of_multientries = collections.defaultdict(list) plt = pretty_plot(16) optim_colors = ['#0000FF', '#FF0000', '#00FF00', '#FFFF00', '#FF00FF', '#FF8080', '#DCDCDC', '#800000', '#FF8000'] optim_font_color = ['#FFFFA0', '#00FFFF', '#FF00FF', '#0000FF', '#00FF00', '#007F7F', '#232323', '#7FFFFF', '#007FFF'] hatch = ['/', '\\', '|', '-', '+', 'o', '*'] (stable, unstable) = self.pourbaix_plot_data(limits) num_of_overlaps = {key: 0 for key in stable.keys()} for entry in stable: if isinstance(entry, MultiEntry): for e in entry.entrylist: if element in e.composition.elements: entry_dict_of_multientries[e.name].append(entry) num_of_overlaps[entry] += 1 else: entry_dict_of_multientries[entry.name].append(entry) if limits: xlim = limits[0] ylim = limits[1] else: xlim = self._analyzer.chempot_limits[0] ylim = self._analyzer.chempot_limits[1] h_line = np.transpose([[xlim[0], -xlim[0] * PREFAC], [xlim[1], -xlim[1] * PREFAC]]) o_line = np.transpose([[xlim[0], -xlim[0] * PREFAC + 1.23], [xlim[1], -xlim[1] * PREFAC + 1.23]]) neutral_line = np.transpose([[7, ylim[0]], [7, ylim[1]]]) V0_line = np.transpose([[xlim[0], 0], [xlim[1], 0]]) ax = plt.gca() ax.set_xlim(xlim) ax.set_ylim(ylim) from pymatgen import Composition, Element from pymatgen.core.ion import Ion def len_elts(entry): if "(s)" in entry: comp = Composition(entry[:-3]) else: comp = Ion.from_formula(entry) return len([el for el in comp.elements if el not in [Element("H"), Element("O")]]) sorted_entry = entry_dict_of_multientries.keys() sorted_entry.sort(key=len_elts) i = -1 label_chr = map(chr, list(range(65, 91))) for entry in sorted_entry: color_indx = 0 x_coord = 0.0 y_coord = 0.0 npts = 0 i += 1 for e in entry_dict_of_multientries[entry]: hc = 0 fc = 0 bc = 0 xy = self.domain_vertices(e) c = self.get_center(stable[e]) x_coord += c[0] y_coord += c[1] npts += 1 color_indx = i if "(s)" in entry: comp = Composition(entry[:-3]) else: comp = Ion.from_formula(entry) if len([el for el in comp.elements if el not in [Element("H"), Element("O")]]) == 1: if color_indx >= len(optim_colors): color_indx = color_indx -\ int(color_indx / len(optim_colors)) * len(optim_colors) patch = Polygon(xy, facecolor=optim_colors[color_indx], closed=True, lw=3.0, fill=True) bc = optim_colors[color_indx] else: if color_indx >= len(hatch): color_indx = color_indx - int(color_indx / len(hatch)) * len(hatch) patch = Polygon(xy, hatch=hatch[color_indx], closed=True, lw=3.0, fill=False) hc = hatch[color_indx] ax.add_patch(patch) xy_center = (x_coord / npts, y_coord / npts) if label_domains: if color_indx >= len(optim_colors): color_indx = color_indx -\ int(color_indx / len(optim_colors)) * len(optim_colors) fc = optim_font_color[color_indx] if bc and not hc: bbox = dict(boxstyle="round", fc=fc) if hc and not bc: bc = 'k' fc = 'w' bbox = dict(boxstyle="round", hatch=hc, fill=False) if bc and hc: bbox = dict(boxstyle="round", hatch=hc, fc=fc) # bbox.set_path_effects([PathEffects.withSimplePatchShadow()]) plt.annotate(latexify_ion(latexify(entry)), xy_center, color=bc, fontsize=30, bbox=bbox) # plt.annotate(label_chr[i], xy_center, # color=bc, fontsize=30, bbox=bbox) lw = 3 plt.plot(h_line[0], h_line[1], "r--", linewidth=lw) plt.plot(o_line[0], o_line[1], "r--", linewidth=lw) plt.plot(neutral_line[0], neutral_line[1], "k-.", linewidth=lw) plt.plot(V0_line[0], V0_line[1], "k-.", linewidth=lw) plt.xlabel("pH") plt.ylabel("E (V)") plt.title(title, fontsize=20, fontweight='bold') return plt
def test_charge_from_formula(self): self.assertEqual(Ion.from_formula("Li+").charge, 1) self.assertEqual(Ion.from_formula("Li[+]").charge, 1) self.assertEqual(Ion.from_formula("Ca[2+]").charge, 2) self.assertEqual(Ion.from_formula("Ca[+2]").charge, 2) self.assertEqual(Ion.from_formula("Ca++").charge, 2) self.assertEqual(Ion.from_formula("Ca[++]").charge, 2) self.assertEqual(Ion.from_formula("Ca2+").charge, 1) self.assertEqual(Ion.from_formula("Cl-").charge, -1) self.assertEqual(Ion.from_formula("Cl[-]").charge, -1) self.assertEqual(Ion.from_formula("SO4[-2]").charge, -2) self.assertEqual(Ion.from_formula("SO4-2").charge, -2) self.assertEqual(Ion.from_formula("SO42-").charge, -1) self.assertEqual(Ion.from_formula("SO4--").charge, -2) self.assertEqual(Ion.from_formula("SO4[--]").charge, -2) self.assertEqual(Ion.from_formula("Na[+-+]").charge, 1)
def get_pourbaix_plot_colorfill_by_element(self, limits=None, title="", label_domains=True, element=None): """ Color domains by element """ from matplotlib.patches import Polygon import matplotlib.patheffects as PathEffects entry_dict_of_multientries = collections.defaultdict(list) plt = get_publication_quality_plot(16) optim_colors = ['#0000FF', '#FF0000', '#00FF00', '#FFFF00', '#FF00FF', '#FF8080', '#DCDCDC', '#800000', '#FF8000'] optim_font_color = ['#FFFFA0', '#00FFFF', '#FF00FF', '#0000FF', '#00FF00', '#007F7F', '#232323', '#7FFFFF', '#007FFF'] hatch = ['/', '\\', '|', '-', '+', 'o', '*'] (stable, unstable) = self.pourbaix_plot_data(limits) num_of_overlaps = {key: 0 for key in stable.keys()} for entry in stable: if isinstance(entry, MultiEntry): for e in entry.entrylist: if element in e.composition.elements: entry_dict_of_multientries[e.name].append(entry) num_of_overlaps[entry] += 1 else: entry_dict_of_multientries[entry.name].append(entry) if limits: xlim = limits[0] ylim = limits[1] else: xlim = self._analyzer.chempot_limits[0] ylim = self._analyzer.chempot_limits[1] h_line = np.transpose([[xlim[0], -xlim[0] * PREFAC], [xlim[1], -xlim[1] * PREFAC]]) o_line = np.transpose([[xlim[0], -xlim[0] * PREFAC + 1.23], [xlim[1], -xlim[1] * PREFAC + 1.23]]) neutral_line = np.transpose([[7, ylim[0]], [7, ylim[1]]]) V0_line = np.transpose([[xlim[0], 0], [xlim[1], 0]]) ax = plt.gca() ax.set_xlim(xlim) ax.set_ylim(ylim) from pymatgen import Composition, Element from pymatgen.core.ion import Ion def len_elts(entry): if "(s)" in entry: comp = Composition(entry[:-3]) else: comp = Ion.from_formula(entry) return len([el for el in comp.elements if el not in [Element("H"), Element("O")]]) sorted_entry = entry_dict_of_multientries.keys() sorted_entry.sort(key=len_elts) i = -1 label_chr = map(chr, range(65, 91)) for entry in sorted_entry: color_indx = 0 x_coord = 0.0 y_coord = 0.0 npts = 0 i += 1 for e in entry_dict_of_multientries[entry]: hc = 0 fc = 0 bc = 0 xy = self.domain_vertices(e) c = self.get_center(stable[e]) x_coord += c[0] y_coord += c[1] npts += 1 color_indx = i if "(s)" in entry: comp = Composition(entry[:-3]) else: comp = Ion.from_formula(entry) if len([el for el in comp.elements if el not in [Element("H"), Element("O")]]) == 1: if color_indx >= len(optim_colors): color_indx = color_indx -\ int(color_indx / len(optim_colors)) * len(optim_colors) patch = Polygon(xy, facecolor=optim_colors[color_indx], closed=True, lw=3.0, fill=True) bc = optim_colors[color_indx] else: if color_indx >= len(hatch): color_indx = color_indx - int(color_indx / len(hatch)) * len(hatch) patch = Polygon(xy, hatch=hatch[color_indx], closed=True, lw=3.0, fill=False) hc = hatch[color_indx] ax.add_patch(patch) xy_center = (x_coord / npts, y_coord / npts) if label_domains: if color_indx >= len(optim_colors): color_indx = color_indx -\ int(color_indx / len(optim_colors)) * len(optim_colors) fc = optim_font_color[color_indx] if bc and not hc: bbox = dict(boxstyle="round", fc=fc) if hc and not bc: bc = 'k' fc = 'w' bbox = dict(boxstyle="round", hatch=hc, fill=False) if bc and hc: bbox = dict(boxstyle="round", hatch=hc, fc=fc) # bbox.set_path_effects([PathEffects.withSimplePatchShadow()]) # plt.annotate(latexify_ion(latexify(entry)), xy_center, # color=fc, fontsize=30, bbox=bbox) plt.annotate(label_chr[i], xy_center, color=bc, fontsize=30, bbox=bbox) lw = 3 plt.plot(h_line[0], h_line[1], "r--", linewidth=lw) plt.plot(o_line[0], o_line[1], "r--", linewidth=lw) plt.plot(neutral_line[0], neutral_line[1], "k-.", linewidth=lw) plt.plot(V0_line[0], V0_line[1], "k-.", linewidth=lw) plt.xlabel("pH") plt.ylabel("E (V)") plt.title(title, fontsize=20, fontweight='bold') return plt
def get_ion( ion: Union[Ion, str], parameter_set: str = "auto", water_model: str = "auto", mixing_rule: Optional[str] = None, ) -> LammpsData: """ Retrieve force field parameters for an ion in water. Args: ion: Formula of the ion (e.g., "Li+"). Not case sensitive. May be passed as either a string or an Ion object. parameter_set: Force field parameters to use for ions. Valid choices are: 1. "jj" for the Jensen and Jorgensen parameters (2006)" 2. "jc" for Joung-Cheatham parameters (2008) 3. "lm" for the Li and Merz group parameters (2020-2021)" The default parameter set is "auto", which assigns a recommended parameter set that is compatible with the chosen water model. water_model: Water model to use. Models must be given as a string (not case sensitive). "-" and "/" are ignored. Hence "tip3pfb" and "TIP3P-FB" are both valid inputs for the TIP3P-FB water model. Available water models are: 1. SPC 2. SPC/E 3. TIP3P-EW 4. TIP3P-FB 5. OPC3 6. TIP4P-EW 7. TIP4P-2005 8. TIP4P-FB 9. OPC The default water model is "auto", which assigns a recommended water model that is compatible with the chosen ion parameters. Other combinations are possible at your own risk. See documentation. When both the parameter_set and water_model are set to "auto", the function returns the Joung-Cheatham parameters for the SPC/E water model. For a systematic comparison of the performance of different water models, refer to Sachini et al., Systematic Comparison of the Structural and Dynamic Properties of Commonly Used Water Models for Molecular Dynamics Simulations. J. Chem. Inf. Model. 2021, 61, 9, 4521–4536. https://doi.org/10.1021/acs.jcim.1c00794 mixing_rule: The mixing rule to use for the ion parameter. Default to None, which does not change the original mixing rule of the parameter set. Available choices are 'LB' (Lorentz-Berthelot or arithmetic) and 'geometric'. If the specified mixing rule does not match the default mixing rule of the parameter set, the output parameter will be converted accordingly. Returns: Force field parameters for the chosen water model. """ alias = { "aq": "aqvist", "jj": "jensen_jorgensen", "jc": "joung_cheatham", "lm": "li_merz" } default_sets = { "spc": "N/A", "spce": "jc", "tip3p": "jc", "tip3pew": "N/A", "tip3pfb": "lm", "opc3": "lm", "tip4p2005": "N/A", "tip4p": "jj", "tip4pew": "jc", "tip4pfb": "lm", "opc": "lm", "jj": "tip4p", "jc": "spce", "lm": "tip4pfb", } water_model = water_model.replace("-", "").replace("/", "").lower() parameter_set = parameter_set.lower() if water_model == "auto" and parameter_set == "auto": water_model = "spce" parameter_set = "jc" elif parameter_set == "auto": parameter_set = default_sets.get(water_model, parameter_set) if parameter_set == "N/A": raise ValueError( f"The {water_model} water model has no specifically parameterized ion parameter sets" "Please try a different water model.") elif water_model == "auto": water_model = default_sets.get(parameter_set, water_model) parameter_set = alias.get(parameter_set, parameter_set) # Make the Ion object to get mass and charge if isinstance(ion, Ion): ion_obj = ion else: ion_obj = Ion.from_formula(ion.capitalize()) # load ion data as a list of IonLJData objects ion_data = loadfn(os.path.join(DATA_DIR, "ion_lj_params.json")) # make sure the ion is in the DataFrame key = ion_obj.reduced_formula filtered_data = [d for d in ion_data if d.formula == key] if len(filtered_data) == 0: raise ValueError( f"Ion {key} not found in database. Please try a different ion." ) # make sure the parameter set is in the DataFrame filtered_data = [ d for d in filtered_data if d.name == parameter_set and d.water_model == water_model ] if len(filtered_data) == 0: raise ValueError( f"No {parameter_set} parameters for water model {water_model} for ion {key}. " "See documentation and try a different combination.") if len(filtered_data) != 1: raise ValueError( f"Something is wrong: multiple ion data entries for {key}, {parameter_set}, and {water_model}" ) # we only consider monatomic ions at present # construct a cubic LammpsBox from a lattice lat = Lattice([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) box = lattice_2_lmpbox(lat)[0] # put it in the center of a cubic Structure struct = Structure(lat, ion_obj, [[0.5, 0.5, 0.5]]) # construct Topology with the ion centered in the box topo = Topology(struct, charges=[ion_obj.charge]) # retrieve Lennard-Jones parameters # construct ForceField object sigma = filtered_data[0].sigma epsilon = filtered_data[0].epsilon if mixing_rule is None: pass else: default_mixing = filtered_data[0].combining_rule water_sigma = WATER_SIGMA.get(filtered_data[0].water_model) if mixing_rule.lower() in [ "lb", "arithmetic", "lorentz-berthelot", "lorentz berthelot" ]: mixing_rule = "LB" elif mixing_rule.lower() == "geometric": mixing_rule = "geometric" else: raise ValueError( "Invalid mixing rule. Supported mixing rules are 'LB'(arithmetic) and 'geometric'. " ) if default_mixing == mixing_rule: pass elif default_mixing == "LB" and mixing_rule == "geometric": sigma = ((water_sigma + sigma) / 2)**2 / water_sigma print( "The parameter mixing rule has been converted from the original 'LB' to 'geometric'.\n" "Please use the parameter set with caution!") else: sigma = 2 * ((water_sigma * sigma)**(1 / 2)) - water_sigma print( "The parameter mixing rule has been converted from the original 'geometric' to 'LB'.\n" "Please use the parameter set with caution!") ff = ForceField([(str(e), e) for e in ion_obj.elements], nonbond_coeffs=[[epsilon, sigma]]) return LammpsData.from_ff_and_topologies(box, ff, [topo], atom_style="full")
def setUp(self): ion = Ion.from_formula("MnO4[-]") self.entry = IonEntry(ion, 49)
def plot_pourbaix_diagram(metastability=0.0, ion_concentration=1e-6, fmt='pdf'): """ Creates a Pourbaix diagram for the material in the cwd. Args: metastability (float): desired metastable tolerance energy (meV/atom). <~50 is generally a sensible range to use. ion_concentration (float): in mol/kg. Sensible values are generally between 1e-8 and 1. fmt (str): matplotlib format style. Check the matplotlib docs for options. """ # Create a ComputedEntry object for the 2D material. composition = Structure.from_file('POSCAR').composition energy = Vasprun('vasprun.xml').final_energy cmpd = ComputedEntry(composition, energy) # Define the chemsys that describes the 2D compound. chemsys = ['O', 'H'] + [elt.symbol for elt in composition.elements if elt.symbol not in ['O', 'H']] # Pick out the ions pertaining to the 2D compound. ion_dict = dict() for elt in chemsys: if elt not in ['O', 'H'] and ION_FORMATION_ENERGIES[elt]: ion_dict.update(ION_FORMATION_ENERGIES[elt]) elements = [Element(elt) for elt in chemsys if elt not in ['O', 'H']] # Add "correction" for metastability cmpd.correction -= float(cmpd.composition.num_atoms)\ * float(metastability) / 1000.0 # Calculate formation energy of the compound from its end # members form_energy = cmpd.energy for elt in composition.as_dict(): form_energy -= CHEMICAL_POTENTIALS[elt] * cmpd.composition[elt] # Convert the compound entry to a pourbaix entry. # Default concentration for solid entries = 1 pbx_cmpd = PourbaixEntry(cmpd) pbx_cmpd.g0_replace(form_energy) pbx_cmpd.reduced_entry() # Add corrected ionic entries to the pourbaix diagram # dft corrections for experimental ionic energies: # Persson et.al PHYSICAL REVIEW B 85, 235438 (2012) pbx_ion_entries = list() # Get PourbaixEntry corresponding to each ion. # Default concentration for ionic entries = 1e-6 # ion_energy = ion_exp_energy + ion_correction * factor # where factor = fraction of element el in the ionic entry # compared to the reference entry for elt in elements: for key in ion_dict: comp = Ion.from_formula(key) if comp.composition[elt] != 0: factor = comp.composition[elt] energy = ion_dict[key] pbx_entry_ion = PourbaixEntry(IonEntry(comp, energy)) pbx_entry_ion.correction = ( ION_CORRECTIONS[elt.symbol] * factor ) pbx_entry_ion.conc = ion_concentration pbx_entry_ion.name = key pbx_ion_entries.append(pbx_entry_ion) # Generate and plot Pourbaix diagram # Each bulk solid/ion has a free energy g of the form: # g = g0_ref + 0.0591 * log10(conc) - nO * mu_H2O + # (nH - 2nO) * pH + phi * (-nH + 2nO + q) all_entries = [pbx_cmpd] + pbx_ion_entries total = sum([composition[el] for el in elements]) comp_dict = {el.symbol: composition[el]/total for el in elements} pourbaix_diagram = PourbaixDiagram(all_entries, comp_dict=comp_dict) plotter = PourbaixPlotter(pourbaix_diagram) # Plotting details... font = "serif" fig = plt.figure(figsize=(14, 9)) ax1 = fig.gca() ax1.set_xlim([0, 14]) ax1.set_xticklabels([int(t) for t in ax1.get_xticks()], fontname=font, fontsize=18) ax1.set_ylim(-2, 2) ax1.set_yticklabels(ax1.get_yticks(), fontname=font, fontsize=18) ax1.set_xlabel("pH", fontname=font, fontsize=18) ax1.set_ylabel("Potential vs. SHE (V)", fontname=font, fontsize=18) # Outline water's stability range. ax1.plot([0, 14], [0, -0.829], color="gray", linestyle="--", alpha=0.7, linewidth=2) ax1.plot([0, 14], [1.229, 0.401], color="gray", linestyle="--", alpha=0.7, linewidth=2) stable_entries = plotter.pourbaix_plot_data( limits=[[0, 14], [-2, 2]])[0] # Add coloring. colors = sb.color_palette("Set2", len(stable_entries)) i = 0 for entry in stable_entries: col = colors[i] i += 1 vertices = plotter.domain_vertices(entry) center_x = sum([v[0] for v in vertices])/len(vertices) center_y = sum([v[1] for v in vertices])/len(vertices) patch = Polygon(vertices, closed=True, fill=True, facecolor=col, linewidth=2, edgecolor="w") ax1.text(center_x, center_y, plotter.print_name(entry), verticalalignment="center", horizontalalignment="center", fontname=font, fontsize=18) ax1.add_patch(patch) plt.savefig("pourbaix.{}".format(fmt)) plt.close()
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 plot_pourbaix_diagram(metastability=0.0, ion_concentration=1e-6, fmt='pdf'): """ Creates a Pourbaix diagram for the material in the cwd. Args: metastability (float): desired metastable tolerance energy (meV/atom). <~50 is generally a sensible range to use. ion_concentration (float): in mol/kg. Sensible values are generally between 1e-8 and 1. fmt (str): matplotlib format style. Check the matplotlib docs for options. """ # Create a ComputedEntry object for the 2D material. composition = Structure.from_file('POSCAR').composition energy = Vasprun('vasprun.xml').final_energy cmpd = ComputedEntry(composition, energy) # Define the chemsys that describes the 2D compound. chemsys = ['O', 'H'] + [ elt.symbol for elt in composition.elements if elt.symbol not in ['O', 'H'] ] # Pick out the ions pertaining to the 2D compound. ion_dict = dict() for elt in chemsys: if elt not in ['O', 'H'] and ION_FORMATION_ENERGIES[elt]: ion_dict.update(ION_FORMATION_ENERGIES[elt]) elements = [Element(elt) for elt in chemsys if elt not in ['O', 'H']] # Add "correction" for metastability cmpd.correction -= float(cmpd.composition.num_atoms)\ * float(metastability) / 1000.0 # Calculate formation energy of the compound from its end # members form_energy = cmpd.energy for elt in composition.as_dict(): form_energy -= CHEMICAL_POTENTIALS[elt] * cmpd.composition[elt] # Convert the compound entry to a pourbaix entry. # Default concentration for solid entries = 1 pbx_cmpd = PourbaixEntry(cmpd) pbx_cmpd.g0_replace(form_energy) pbx_cmpd.reduced_entry() # Add corrected ionic entries to the pourbaix diagram # dft corrections for experimental ionic energies: # Persson et.al PHYSICAL REVIEW B 85, 235438 (2012) pbx_ion_entries = list() # Get PourbaixEntry corresponding to each ion. # Default concentration for ionic entries = 1e-6 # ion_energy = ion_exp_energy + ion_correction * factor # where factor = fraction of element el in the ionic entry # compared to the reference entry for elt in elements: for key in ion_dict: comp = Ion.from_formula(key) if comp.composition[elt] != 0: factor = comp.composition[elt] energy = ion_dict[key] pbx_entry_ion = PourbaixEntry(IonEntry(comp, energy)) pbx_entry_ion.correction = (ION_CORRECTIONS[elt.symbol] * factor) pbx_entry_ion.conc = ion_concentration pbx_entry_ion.name = key pbx_ion_entries.append(pbx_entry_ion) # Generate and plot Pourbaix diagram # Each bulk solid/ion has a free energy g of the form: # g = g0_ref + 0.0591 * log10(conc) - nO * mu_H2O + # (nH - 2nO) * pH + phi * (-nH + 2nO + q) all_entries = [pbx_cmpd] + pbx_ion_entries pourbaix = PourbaixDiagram(all_entries) # Analysis features # panalyzer = PourbaixAnalyzer(pourbaix) # instability = panalyzer.get_e_above_hull(pbx_cmpd) plotter = PourbaixPlotter(pourbaix) plot = plotter.get_pourbaix_plot(limits=[[0, 14], [-2, 2]], label_domains=True) fig = plot.gcf() ax1 = fig.gca() # Add coloring to highlight the stability region for the 2D # material, if one exists. stable_entries = plotter.pourbaix_plot_data(limits=[[0, 14], [-2, 2]])[0] for entry in stable_entries: if entry == pbx_cmpd: col = plt.cm.Blues(0) else: col = plt.cm.rainbow( float(ION_COLORS[entry.composition.reduced_formula])) vertices = plotter.domain_vertices(entry) patch = Polygon(vertices, closed=True, fill=True, color=col) ax1.add_patch(patch) fig.set_size_inches((11.5, 9)) plot.tight_layout(pad=1.09) # Save plot if metastability: plot.suptitle('Metastable Tolerance =' ' {} meV/atom'.format(metastability), fontsize=20) plot.savefig('{}_{}.{}'.format(composition.reduced_formula, ion_concentration, fmt), transparent=True) else: plot.savefig('{}_{}.{}'.format(composition.reduced_formula, ion_concentration, fmt), transparent=True) plot.close()
def len_elts(entry): comp = Composition(entry[:-3]) if "(s)" in entry else Ion.from_formula(entry) return len(set(comp.elements) - {Element("H"), Element("O")})
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 pbx_entry_ion.conc = 1 pbx_solid_entries.append(pbx_entry_ion) for a in pbx_solid_entries: print(a, a.conc) print('hkjo') #__| # +
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
def len_elts(entry): comp = Composition( entry[:-3]) if "(s)" in entry else Ion.from_formula(entry) return len(set(comp.elements) - {Element("H"), Element("O")})
def test_get_composition(self): comp = self.entry.composition expected_comp = Ion.from_formula('MnO4[-]') self.assertEqual(comp, expected_comp, "Wrong composition!")
def plot_pourbaix_diagram(metastability=0.0, ion_concentration=1e-6, fmt='pdf'): """ Creates a Pourbaix diagram for the material in the cwd. Args: metastability (float): desired metastable tolerance energy (meV/atom). <~50 is generally a sensible range to use. ion_concentration (float): in mol/kg. Sensible values are generally between 1e-8 and 1. fmt (str): matplotlib format style. Check the matplotlib docs for options. """ # Create a ComputedEntry object for the 2D material. composition = Structure.from_file('POSCAR').composition energy = Vasprun('vasprun.xml').final_energy cmpd = ComputedEntry(composition, energy) # Define the chemsys that describes the 2D compound. chemsys = ['O', 'H'] + [elt.symbol for elt in composition.elements if elt.symbol not in ['O', 'H']] # Experimental ionic energies # See ions.yaml for ion formation energies and references. exp_dict = ION_DATA['ExpFormEnergy'] ion_correction = ION_DATA['IonCorrection'] # Pick out the ions pertaining to the 2D compound. ion_dict = dict() for elt in chemsys: if elt not in ['O', 'H'] and exp_dict[elt]: ion_dict.update(exp_dict[elt]) elements = [Element(elt) for elt in chemsys if elt not in ['O', 'H']] # Add "correction" for metastability cmpd.correction -= float(cmpd.composition.num_atoms)\ * float(metastability) / 1000.0 # Calculate formation energy of the compound from its end # members form_energy = cmpd.energy for elt in composition.as_dict(): form_energy -= END_MEMBERS[elt] * cmpd.composition[elt] # Convert the compound entry to a pourbaix entry. # Default concentration for solid entries = 1 pbx_cmpd = PourbaixEntry(cmpd) pbx_cmpd.g0_replace(form_energy) pbx_cmpd.reduced_entry() # Add corrected ionic entries to the pourbaix diagram # dft corrections for experimental ionic energies: # Persson et.al PHYSICAL REVIEW B 85, 235438 (2012) pbx_ion_entries = list() # Get PourbaixEntry corresponding to each ion. # Default concentration for ionic entries = 1e-6 # ion_energy = ion_exp_energy + ion_correction * factor # where factor = fraction of element el in the ionic entry # compared to the reference entry for elt in elements: for key in ion_dict: comp = Ion.from_formula(key) if comp.composition[elt] != 0: factor = comp.composition[elt] energy = ion_dict[key] pbx_entry_ion = PourbaixEntry(IonEntry(comp, energy)) pbx_entry_ion.correction = ion_correction[elt.symbol]\ * factor pbx_entry_ion.conc = ion_concentration pbx_entry_ion.name = key pbx_ion_entries.append(pbx_entry_ion) # Generate and plot Pourbaix diagram # Each bulk solid/ion has a free energy g of the form: # g = g0_ref + 0.0591 * log10(conc) - nO * mu_H2O + # (nH - 2nO) * pH + phi * (-nH + 2nO + q) all_entries = [pbx_cmpd] + pbx_ion_entries pourbaix = PourbaixDiagram(all_entries) # Analysis features panalyzer = PourbaixAnalyzer(pourbaix) # instability = panalyzer.get_e_above_hull(pbx_cmpd) plotter = PourbaixPlotter(pourbaix) plot = plotter.get_pourbaix_plot(limits=[[0, 14], [-2, 2]], label_domains=True) fig = plot.gcf() ax1 = fig.gca() # Add coloring to highlight the stability region for the 2D # material, if one exists. stable_entries = plotter.pourbaix_plot_data( limits=[[0, 14], [-2, 2]])[0] for entry in stable_entries: if entry == pbx_cmpd: col = plt.cm.Blues(0) else: col = plt.cm.rainbow(float( ION_COLORS[entry.composition.reduced_formula])) vertices = plotter.domain_vertices(entry) patch = Polygon(vertices, closed=True, fill=True, color=col) ax1.add_patch(patch) fig.set_size_inches((11.5, 9)) plot.tight_layout(pad=1.09) # Save plot if metastability: plot.suptitle('Metastable Tolerance =' ' {} meV/atom'.format(metastability), fontsize=20) plot.savefig('{}_{}.{}'.format( composition.reduced_formula, ion_concentration, fmt), transparent=True) else: plot.savefig('{}_{}.{}'.format(composition.reduced_formula, ion_concentration, fmt), transparent=True) plot.close()
def test_get_composition(self): comp = self.entry.composition expected_comp = Ion.from_formula('MnO4[-]') self.assertEquals(comp, expected_comp, "Wrong composition!")