def get_electrochemical_stability(mpid, pH, potential): ''' A wrapper for pymatgen to construct Pourbaix amd calculate electrochemical stability under reaction condition (i.e. at a given pH and applied potential). Arg: mpid Materials project ID of a bulk composition. e.g. Pt: 'mp-126'. pH: pH at reaction condition. Commonly ones are: acidic: pH=0, neutral: pH=7, and basic pH=14. potential: Applied potential at reaction condition. Returns: stability Electrochemical stability of a composition under reaction condition, unit is eV/atom. ''' mpr = MPRester(read_rc('matproj_api_key')) try: entry = mpr.get_entries(mpid)[0] composition = entry.composition comp_dict = {str(key): value for key, value in composition.items() if key not in ELEMENTS_HO} entries = mpr.get_pourbaix_entries(list(comp_dict.keys())) entry = [entry for entry in entries if entry.entry_id == mpid][0] pbx = PourbaixDiagram(entries, comp_dict=comp_dict, filter_solids=False) stability = pbx.get_decomposition_energy(entry, pH=pH, V=potential) stability = round(stability, 3) # Some mpid's stability are not available except IndexError: stability = np.nan return stability
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.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)
def test_multicomponent(self): # Assure no ions get filtered at high concentration ag_n = [ e for e in self.test_data['Ag-Te-N'] if not "Te" in e.composition ] highconc = PourbaixDiagram(ag_n, filter_solids=True, conc_dict={ "Ag": 1e-5, "N": 1 }) entry_sets = [set(e.entry_id) for e in highconc.stable_entries] self.assertIn({"mp-124", "ion-17"}, entry_sets) # Binary system pd_binary = PourbaixDiagram(self.test_data['Ag-Te'], filter_solids=True, comp_dict={ "Ag": 0.5, "Te": 0.5 }, conc_dict={ "Ag": 1e-8, "Te": 1e-8 }) self.assertEqual(len(pd_binary.stable_entries), 30) test_entry = pd_binary.find_stable_entry(8, 2) self.assertTrue("mp-499" in test_entry.entry_id) # Find a specific multientry to test self.assertEqual(pd_binary.get_decomposition_energy(test_entry, 8, 2), 0) self.assertEqual( pd_binary.get_decomposition_energy(test_entry.entry_list[0], 8, 2), 0) pd_ternary = PourbaixDiagram(self.test_data['Ag-Te-N'], filter_solids=True) self.assertEqual(len(pd_ternary.stable_entries), 49) ag = self.test_data['Ag-Te-N'][30] self.assertAlmostEqual(pd_ternary.get_decomposition_energy(ag, 2, -1), 0) self.assertAlmostEqual(pd_ternary.get_decomposition_energy(ag, 10, -2), 0)
def test_multicomponent(self): # Binary system pd_binary = PourbaixDiagram(self.test_data['Ag-Te'], filter_solids=True, comp_dict={"Ag": 0.5, "Te": 0.5}, conc_dict={"Ag": 1e-8, "Te": 1e-8}) self.assertEqual(len(pd_binary.stable_entries), 30) test_entry = pd_binary.find_stable_entry(8, 2) self.assertTrue("mp-499" in test_entry.entry_id) # Find a specific multientry to test self.assertEqual(pd_binary.get_decomposition_energy(test_entry, 8, 2), 0) self.assertEqual(pd_binary.get_decomposition_energy( test_entry.entry_list[0], 8, 2), 0) pd_ternary = PourbaixDiagram(self.test_data['Ag-Te-N'], filter_solids=True) self.assertEqual(len(pd_ternary.stable_entries), 49) ag = self.test_data['Ag-Te-N'][30] self.assertAlmostEqual(pd_ternary.get_decomposition_energy(ag, 2, -1), 0) self.assertAlmostEqual(pd_ternary.get_decomposition_energy(ag, 10, -2), 0)
def test_multicomponent(self): # Assure no ions get filtered at high concentration ag_n = [e for e in self.test_data['Ag-Te-N'] if not "Te" in e.composition] highconc = PourbaixDiagram(ag_n, filter_solids=True, conc_dict={"Ag": 1e-5, "N": 1}) entry_sets = [set(e.entry_id) for e in highconc.stable_entries] self.assertIn({"mp-124", "ion-17"}, entry_sets) # Binary system pd_binary = PourbaixDiagram(self.test_data['Ag-Te'], filter_solids=True, comp_dict={"Ag": 0.5, "Te": 0.5}, conc_dict={"Ag": 1e-8, "Te": 1e-8}) self.assertEqual(len(pd_binary.stable_entries), 30) test_entry = pd_binary.find_stable_entry(8, 2) self.assertTrue("mp-499" in test_entry.entry_id) # Find a specific multientry to test self.assertEqual(pd_binary.get_decomposition_energy(test_entry, 8, 2), 0) self.assertEqual(pd_binary.get_decomposition_energy( test_entry.entry_list[0], 8, 2), 0) pd_ternary = PourbaixDiagram(self.test_data['Ag-Te-N'], filter_solids=True) self.assertEqual(len(pd_ternary.stable_entries), 49) ag = self.test_data['Ag-Te-N'][30] self.assertAlmostEqual(pd_ternary.get_decomposition_energy(ag, 2, -1), 0) self.assertAlmostEqual(pd_ternary.get_decomposition_energy(ag, 10, -2), 0) # Test invocation of pourbaix diagram from ternary data new_ternary = PourbaixDiagram(pd_ternary.all_entries) self.assertEqual(len(new_ternary.stable_entries), 49) self.assertAlmostEqual(new_ternary.get_decomposition_energy(ag, 2, -1), 0) self.assertAlmostEqual(new_ternary.get_decomposition_energy(ag, 10, -2), 0)
def test_multicomponent(self): # Assure no ions get filtered at high concentration ag_n = [e for e in self.test_data["Ag-Te-N"] if "Te" not in e.composition] highconc = PourbaixDiagram( ag_n, filter_solids=True, conc_dict={"Ag": 1e-5, "N": 1} ) entry_sets = [set(e.entry_id) for e in highconc.stable_entries] self.assertIn({"mp-124", "ion-17"}, entry_sets) # Binary system pd_binary = PourbaixDiagram( self.test_data["Ag-Te"], filter_solids=True, comp_dict={"Ag": 0.5, "Te": 0.5}, conc_dict={"Ag": 1e-8, "Te": 1e-8}, ) self.assertEqual(len(pd_binary.stable_entries), 30) test_entry = pd_binary.find_stable_entry(8, 2) self.assertTrue("mp-499" in test_entry.entry_id) # Find a specific multientry to test self.assertEqual(pd_binary.get_decomposition_energy(test_entry, 8, 2), 0) pd_ternary = PourbaixDiagram(self.test_data["Ag-Te-N"], filter_solids=True) self.assertEqual(len(pd_ternary.stable_entries), 49) # Fetch a solid entry and a ground state entry mixture ag_te_n = self.test_data["Ag-Te-N"][-1] ground_state_ag_with_ions = MultiEntry( [self.test_data["Ag-Te-N"][i] for i in [4, 18, 30]], weights=[1 / 3, 1 / 3, 1 / 3], ) self.assertAlmostEqual( pd_ternary.get_decomposition_energy(ag_te_n, 2, -1), 2.767822855765 ) self.assertAlmostEqual( pd_ternary.get_decomposition_energy(ag_te_n, 10, -2), 3.756840056890625 ) self.assertAlmostEqual( pd_ternary.get_decomposition_energy(ground_state_ag_with_ions, 2, -1), 0 ) # Test invocation of pourbaix diagram from ternary data new_ternary = PourbaixDiagram(pd_ternary.all_entries) self.assertEqual(len(new_ternary.stable_entries), 49) self.assertAlmostEqual( new_ternary.get_decomposition_energy(ag_te_n, 2, -1), 2.767822855765 ) self.assertAlmostEqual( new_ternary.get_decomposition_energy(ag_te_n, 10, -2), 3.756840056890625 ) self.assertAlmostEqual( new_ternary.get_decomposition_energy(ground_state_ag_with_ions, 2, -1), 0 )
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), 29) 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_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 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 test_solid_filter(self): entries = self.test_data['Ag-Te-N'] pbx = PourbaixDiagram(entries, filter_solids=True) pbx.get_decomposition_energy(entries[0], 0, 0)
def test_nofilter(self): entries = self.test_data['Ag-Te'] pbx = PourbaixDiagram(entries) pbx.get_decomposition_energy(entries[0], 0, 0)
def test_solid_filter(self): entries = self.test_data['Ag-Te-N'] pbx = PourbaixDiagram(entries, filter_solids=True) pbx.get_decomposition_energy(entries[0], 0, 0)
def test_nofilter(self): entries = self.test_data['Ag-Te'] pbx = PourbaixDiagram(entries) pbx.get_decomposition_energy(entries[0], 0, 0)
# Import necessary tools from pymatgen from pymatgen import MPRester from pymatgen.analysis.pourbaix_diagram import PourbaixDiagram, PourbaixPlotter # %matplotlib inline # Initialize the MP Rester mpr = MPRester("hvSDrzMafUtXE0JQ") ##将API 秘钥输入适配器中,并且初始化适配器,需要自己申请。 # Get all pourbaix entries corresponding to the Cu-O-H chemical system. entries = mpr.get_pourbaix_entries(["Cu"]) # Construct the PourbaixDiagram object pbx = PourbaixDiagram(entries) # Get an entry stability as a function of pH and V entry = [e for e in entries if e.entry_id == 'mp-1692'][0] print( "CuO's potential energy per atom relative to the most", "stable decomposition product is {:0.2f} eV/atom".format( pbx.get_decomposition_energy(entry, pH=7, V=-0.2))) plotter = PourbaixPlotter(pbx) plotter.get_pourbaix_plot().show() plt = plotter.plot_entry_stability(entry) plt.show() # Get all pourbaix entries corresponding to the Fe-O-H chemical system. entries = mpr.get_pourbaix_entries(["Bi", "V"]) # Construct the PourbaixDiagram object pbx = PourbaixDiagram(entries, comp_dict={ "Bi": 0.5, "V": 0.5 }, conc_dict={ "Bi": 1e-8, "V": 1e-8
def get_figure(pourbaix_diagram: PourbaixDiagram, heatmap_entry=None, show_water_lines=True) -> go.Figure: """ Static method for getting plotly figure from a Pourbaix diagram. Args: pourbaix_diagram (PourbaixDiagram): Pourbaix diagram to plot heatmap_entry (PourbaixEntry): id for the heatmap generation show_water_lines (bool): if True, show region of water stability Returns: (dict) figure layout """ data = [] shapes = [] xydata = [] labels = [] domain_heights = [] include_legend = set() for entry, vertices in pourbaix_diagram._stable_domain_vertices.items( ): formula = entry.name clean_formula = PourbaixDiagramComponent.clean_formula(formula) # Generate information for textual labels domain = Polygon(vertices) centroid = domain.centroid height = domain.bounds[3] - domain.bounds[1] # Ensure label is within plot area # TODO: remove hard-coded value here and make sure it's set dynamically xydata.append([centroid.x, centroid.y]) labels.append(clean_formula) domain_heights.append(height) # Assumes that entry.phase_type is either "Solid" or "Ion" or # a list of "Solid" and "Ion" if isinstance(entry.phase_type, str): legend_entry = entry.phase_type elif isinstance(entry.phase_type, list): if len(set(entry.phase_type)) == 1: legend_entry = ("Mixed Ion" if entry.phase_type[0] == "Ion" else "Mixed Solid") else: legend_entry = "Mixed Ion/Solid" else: # Should never get here print(f"Debug required in Pourbaix for {entry.phase_type}") legend_entry = "Unknown" if not heatmap_entry: if legend_entry == "Ion" or legend_entry == "Unknown": fillcolor = "rgb(255,255,250,1)" # same color as old website elif legend_entry == "Mixed Ion": fillcolor = "rgb(255,255,240,1)" elif legend_entry == "Solid": fillcolor = "rgba(188,236,237,1)" # same color as old website elif legend_entry == "Mixed Solid": fillcolor = "rgba(155,229,232,1)" elif legend_entry == "Mixed Ion/Solid": fillcolor = "rgb(95,238,222,1)" else: fillcolor = "rgba(0,0,0,0)" data.append( go.Scatter( x=[v[0] for v in vertices], y=[v[1] for v in vertices], fill="toself", fillcolor=fillcolor, legendgroup=legend_entry, # legendgrouptitle={"text": legend_entry}, name=legend_entry, text=f"{clean_formula} ({entry.entry_id})", marker={"color": "Black"}, line={ "color": "Black", "width": 0 }, mode="lines", showlegend=True if legend_entry not in include_legend else False, )) include_legend.add(legend_entry) # Add lines separately so they show up on heatmap # Info on SVG paths: https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths # Move to first point path = "M {},{}".format(*vertices[0]) # Draw lines to each other point path += "".join( ["L {},{}".format(*vertex) for vertex in vertices[1:]]) # Close path path += "Z" # stable entries are black with default color scheme, # so use white lines instead if heatmap_entry: line = {"color": "White", "width": 4} else: line = {"color": "Black", "width": 1} shape = go.layout.Shape( type="path", path=path, fillcolor="rgba(0,0,0,0)", opacity=1, line=line, ) shapes.append(shape) layout = PourbaixDiagramComponent.default_plot_style layout.update({"shapes": shapes}) if heatmap_entry is None: x, y = zip(*xydata) data.append( go.Scatter(x=x, y=y, text=labels, hoverinfo="text", mode="text", name="Labels")) layout.update({"annotations": []}) else: # Add annotations to layout to make text more readable when displaying heatmaps # TODO: this doesn't work yet; resolve or scrap # cmap = get_cmap(PourbaixDiagramComponent.colorscale) # def get_text_color(x, y): # """ # Set text color based on whether background at that point is dark or light. # """ # energy = pourbaix_diagram.get_decomposition_energy(entry, pH=x, V=y) # c = [int(c * 255) for c in cmap(energy)[0:3]] # # borrowed from crystal_toolkit.components.structure # # TODO: move to utility function and ensure correct attribution for magic numbers # if 1 - (c[0] * 0.299 + c[1] * 0.587 + c[2] * 0.114) / 255 < 0.5: # font_color = "#000000" # else: # font_color = "#ffffff" # #print(energy, c, font_color) # return font_color def get_text_size(available_vertical_space): """ Set text size based on available vertical space """ return min(max(6 * available_vertical_space, 12), 20) annotations = [ { "align": "center", "bgcolor": "white", "font": { "color": "black", "size": get_text_size(height) }, "opacity": 1, "showarrow": False, "text": label, "x": x, "xanchor": "center", "yanchor": "auto", # "xshift": -10, # "yshift": -10, "xref": "x", "y": y, "yref": "y", } for (x, y), label, height in zip(xydata, labels, domain_heights) ] layout.update({"annotations": annotations}) # Get data for heatmap if heatmap_entry is not None: ph_range = np.arange(-2, 16.001, 0.1) v_range = np.arange(-2, 4.001, 0.1) ph_mesh, v_mesh = np.meshgrid(ph_range, v_range) decomposition_e = pourbaix_diagram.get_decomposition_energy( heatmap_entry, ph_mesh, v_mesh) # Generate hoverinfo hovertexts = [] for ph_val, v_val, de_val in zip(ph_mesh.ravel(), v_mesh.ravel(), decomposition_e.ravel()): hovertext = [ "∆G<sub>pbx</sub>={:.2f}".format(de_val), "ph={:.2f}".format(ph_val), "V={:.2f}".format(v_val), ] hovertext = "<br>".join(hovertext) hovertexts.append(hovertext) hovertexts = np.reshape(hovertexts, list(decomposition_e.shape)) # Enforce decomposition limit energy decomposition_e = np.min( [decomposition_e, np.ones(decomposition_e.shape)], axis=0) heatmap_formula = unicodeify( Composition(heatmap_entry.composition).reduced_formula) hmap = go.Contour( z=decomposition_e, x=list(ph_range), y=list(v_range), colorbar={ "title": "∆G<sub>pbx</sub> (eV/atom)", "titleside": "right", }, colorscale=PourbaixDiagramComponent.colorscale, # or magma zmin=0, zmax=1, connectgaps=True, line_smoothing=0, line_width=0, # contours_coloring="heatmap", text=hovertexts, name=f"{heatmap_formula} ({heatmap_entry.entry_id}) Heatmap", showlegend=True, ) data.append(hmap) if show_water_lines: ph_range = [-2, 16] # hydrogen line data.append( go.Scatter( x=[ph_range[0], ph_range[1]], y=[-ph_range[0] * PREFAC, -ph_range[1] * PREFAC], mode="lines", line={ "color": "orange", "dash": "dash" }, name="Hydrogen Stability Line", )) # oxygen line data.append( go.Scatter( x=[ph_range[0], ph_range[1]], y=[ -ph_range[0] * PREFAC + 1.23, -ph_range[1] * PREFAC + 1.23 ], mode="lines", line={ "color": "orange", "dash": "dash" }, name="Oxygen Stability Line", )) # 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]]) # # SHE line # # data.append(go.Scatter( # # # # )) figure = go.Figure(data=data, layout=layout) return figure