def update_heatmap_choices(entries): if not entries: raise PreventUpdate options = [] for entry in entries: if entry["entry_id"].startswith("mp"): composition = Composition(entry["entry"]["composition"]) formula = unicodeify(composition.reduced_formula) mpid = entry["entry_id"] options.append({ "label": f"{formula} ({mpid})", "value": mpid }) heatmap_options = self.get_choice_input( "heatmap_choice", state={}, label="Heatmap Entry", help_str="Choose the entry to use for heatmap generation.", options=options, disabled=False, ) return heatmap_options
def _log_structure_information(structure: Structure, symprec): log_banner("STRUCTURE") logger.info("Structure information:") comp = structure.composition lattice = structure.lattice formula = comp.get_reduced_formula_and_factor(iupac_ordering=True)[0] if not symprec: symprec = 1e-32 sga = SpacegroupAnalyzer(structure, symprec=symprec) spg = unicodeify_spacegroup(sga.get_space_group_symbol()) comp_info = [ "formula: {}".format(unicodeify(formula)), "# sites: {}".format(structure.num_sites), "space group: {}".format(spg), ] log_list(comp_info) logger.info("Lattice:") lattice_info = [ "a, b, c [Å]: {:.2f}, {:.2f}, {:.2f}".format(*lattice.abc), "α, β, γ [°]: {:.0f}, {:.0f}, {:.0f}".format(*lattice.angles), ] log_list(lattice_info)
def update_info_box(clickData, x_prop, y_prop, z_prop, color_prop): if clickData is None: raise PreventUpdate point = clickData['points'][0] mpid = point['text'] x = point['x'] y = point['y'] print(point) s = mpr.get_structure_by_material_id(mpid) formula = unicodeify(s.composition.reduced_formula) info = f""" ### {formula} ##### [{mpid}](https://materialsproject.org/materials/{mpid}) x = {x:.2f} {scalar_symbols[x_prop].unit_as_string} y = {y:.2f} {scalar_symbols[y_prop].unit_as_string} """ if 'z' in point: z = point['z'] info += f"z = {z:.2f} {scalar_symbols[z_prop].unit_as_string}" if 'marker.color' in point: c = point['marker.color'] info += f"c = {c:.2f} {scalar_symbols[color_prop].unit_as_string}" return info
def update_info_box(clickData, x_prop, y_prop): point = clickData['points'][0] mpid = point['text'] x = point['x'] y = point['y'] s = mpr.get_structure_by_material_id(mpid) formula = unicodeify(s.composition.reduced_formula) return f"""
def get_all_component_descriptions(self) -> str: """Gets the descriptions of all components in the structure. Returns: A description of all components in the structure. """ if len(self._da.components) == 1: return self.get_component_description( self._da.get_component_groups()[0].components[0].index, single_component=True, ) else: component_groups = self._da.get_component_groups() component_descriptions = [] for group in component_groups: for component in group.components: if group.molecule_name: # don't describe known molecules continue formula = group.formula group_count = group.count component_count = component.count shape = dimensionality_to_shape[group.dimensionality] if self.fmt == "latex": formula = latexify(formula) elif self.fmt == "unicode": formula = unicodeify(formula) elif self.fmt == "html": formula = htmlify(formula) if group_count == component_count: s_filler = "the" if group_count == 1 else "each" else: s_filler = "{} of the".format( en.number_to_words(component_count) ) shape = en.plural(shape) desc = f"In {s_filler} {formula} {shape}, " desc += self.get_component_description(component.index) component_descriptions.append(desc) return " ".join(component_descriptions)
def get_mineral_description(self) -> str: """Gets the mineral name and space group description. If the structure is a perfect match for a known prototype (e.g. the distance parameter is -1, the mineral name is the prototype name. If a structure is not a perfect match but similar to a known mineral, "-like" will be added to the mineral name. If the structure is a good match to a mineral but contains a different number of element types than the mineral prototype, "-derived" will be added to the mineral name. Returns: The description of the mineral name. """ spg_symbol = self._da.spg_symbol formula = self._da.formula if self.fmt == "latex": spg_symbol = latexify_spacegroup(self._da.spg_symbol) formula = latexify(formula) elif self.fmt == "unicode": spg_symbol = unicodeify_spacegroup(self._da.spg_symbol) formula = unicodeify(formula) elif self.fmt == "html": spg_symbol = htmlify_spacegroup(self._da.spg_symbol) formula = htmlify(formula) mineral_name = get_mineral_name(self._da.mineral) if mineral_name: desc = f"{formula} is {mineral_name} structured and" else: desc = f"{formula}" desc += " crystallizes in the {} {} space group.".format( self._da.crystal_system, spg_symbol ) return desc
def update_info_box(clickData, x_prop, y_prop, z_prop, color_prop): if clickData is None: raise PreventUpdate point = clickData['points'][0] mpid = point['text'] x = point['x'] y = point['y'] logger.debug(f"User clicked: {point}") try: s = mpr.get_structure_by_material_id(mpid) except IndexError: logger.error(f"{mpid} cannot be found on MP. Database refresh needed?") return f"{mpid} cannot be found on Materials Project. " \ f"It may have been deprecated. This error has been logged." formula = unicodeify(s.composition.reduced_formula) info = f""" ### {formula} ##### [{mpid}](https://materialsproject.org/materials/{mpid}) x = {x:.2f} {scalar_symbols[x_prop].unit_as_string} y = {y:.2f} {scalar_symbols[y_prop].unit_as_string} """ if 'z' in point: z = point['z'] info += f"z = {z:.2f} {scalar_symbols[z_prop].unit_as_string}" if 'marker.color' in point: c = point['marker.color'] info += f"c = {c:.2f} {scalar_symbols[color_prop].unit_as_string}" return info
def test_unicodeify(self): self.assertEqual(unicodeify("Li3Fe2(PO4)3"), "Li₃Fe₂(PO₄)₃") self.assertRaises(ValueError, unicodeify, "Li0.2Na0.8Cl") self.assertEqual(unicodeify_species("O2+"), "O²⁺") self.assertEqual(unicodeify_spacegroup("F-3m"), "F3̅m")
def _get_poly_site_description(self, site_index: int): """Gets a description of a connected polyhedral site. If the site likeness (order parameter) is less than ``distorted_tol``, "distorted" will be added to the geometry description. Args: site_index: An inequivalent site index. Returns: A description the a polyhedral site, including connectivity. """ site = self._da.sites[site_index] nnn_details = self._da.get_next_nearest_neighbor_details( site_index, group=not self.describe_symmetry_labels ) from_element = get_formatted_el( site["element"], self._da.sym_labels[site_index], use_oxi_state=self.describe_oxidation_state, use_sym_label=self.describe_symmetry_labels, fmt=self.fmt, ) from_poly_formula = site["poly_formula"] if self.fmt == "latex": from_poly_formula = latexify(from_poly_formula) elif self.fmt == "unicode": from_poly_formula = unicodeify(from_poly_formula) elif self.fmt == "html": from_poly_formula = htmlify(from_poly_formula) s_from_poly_formula = get_el(site["element"]) + from_poly_formula if site["geometry"]["likeness"] < self.distorted_tol: s_distorted = "distorted " else: s_distorted = "" s_polyhedra = geometry_to_polyhedra[site["geometry"]["type"]] s_polyhedra = polyhedra_plurals[s_polyhedra] nn_desc = self._get_nearest_neighbor_description(site_index) desc = f"{from_element} is bonded to {nn_desc} to form " # handle the case we were are connected to the same type of polyhedra if ( nnn_details[0].element == site["element"] and len( {(nnn_site.element, nnn_site.poly_formula) for nnn_site in nnn_details} ) ) == 1: connectivities = list({nnn_site.connectivity for nnn_site in nnn_details}) s_mixture = "a mixture of " if len(connectivities) != 1 else "" s_connectivities = en.join(connectivities) desc += "{}{}{}-sharing {} {}".format( s_mixture, s_distorted, s_connectivities, s_from_poly_formula, s_polyhedra, ) return desc # otherwise loop through nnn connectivities and describe individually desc += "{}{} {} that share ".format( s_distorted, s_from_poly_formula, s_polyhedra ) nnn_descriptions = [] for nnn_site in nnn_details: to_element = get_formatted_el( nnn_site.element, nnn_site.sym_label, use_oxi_state=False, use_sym_label=self.describe_symmetry_labels, ) to_poly_formula = nnn_site.poly_formula if self.fmt == "latex": to_poly_formula = latexify(to_poly_formula) elif self.fmt == "unicode": to_poly_formula = unicodeify(to_poly_formula) elif self.fmt == "html": to_poly_formula = htmlify(to_poly_formula) to_poly_formula = to_element + to_poly_formula to_shape = geometry_to_polyhedra[nnn_site.geometry] if len(nnn_site.sites) == 1 and nnn_site.count != 1: s_equivalent = " equivalent " else: s_equivalent = " " if nnn_site.count == 1: s_an = f" {en.an(nnn_site.connectivity)}" else: s_an = "" to_shape = polyhedra_plurals[to_shape] nnn_descriptions.append( "{}{} with {}{}{} {}".format( s_an, en.plural(nnn_site.connectivity, nnn_site.count), en.number_to_words(nnn_site.count), s_equivalent, to_poly_formula, to_shape, ) ) return desc + en.join(nnn_descriptions)
def get_component_makeup_summary(self) -> str: """Gets a summary of the makeup of components in a structure. Returns: A description of the number of components and their dimensionalities and orientations. """ component_groups = self._da.get_component_groups() if ( len(component_groups) == 1 and component_groups[0].count == 1 and component_groups[0].dimensionality == 3 ): desc = "" else: if self._da.dimensionality == 3: desc = "The structure consists of " else: desc = "The structure is {}-dimensional and consists of " "".format( en.number_to_words(self._da.dimensionality) ) component_makeup_summaries = [] nframeworks = len( [ c for g in component_groups for c in g.components if c.dimensionality == 3 ] ) for component_group in component_groups: if nframeworks == 1 and component_group.dimensionality == 3: s_count = "a" else: s_count = en.number_to_words(component_group.count) dimensionality = component_group.dimensionality if component_group.molecule_name: if component_group.nsites == 1: shape = "atom" else: shape = "molecule" shape = en.plural(shape, s_count) formula = component_group.molecule_name else: shape = en.plural(dimensionality_to_shape[dimensionality], s_count) formula = component_group.formula if self.fmt == "latex": formula = latexify(formula) elif self.fmt == "unicode": formula = unicodeify(formula) print(formula) elif self.fmt == "html": formula = htmlify(formula) comp_desc = f"{s_count} {formula} {shape}" if component_group.dimensionality in [1, 2]: orientations = list( {c.orientation for c in component_group.components} ) s_direction = en.plural("direction", len(orientations)) comp_desc += " oriented in the {} {}".format( en.join(orientations), s_direction ) component_makeup_summaries.append(comp_desc) if nframeworks == 1 and len(component_makeup_summaries) > 1: # when there is a single framework, make the description read # "... and 8 Sn atoms inside a SnO2 framework" instead of # "..., 8 Sn atoms and one SnO2 framework" # This works because the component summaries are sorted by # dimensionality desc += en.join(component_makeup_summaries[:-1]) desc += f" inside {component_makeup_summaries[-1]}." else: desc += en.join(component_makeup_summaries) + "." return desc
def test_unicodeify(self): self.assertEqual(unicodeify("Li3Fe2(PO4)3"), "Li₃Fe₂(PO₄)₃") self.assertRaises(ValueError, unicodeify, "Li0.2Na0.8Cl")
def test_unicodeify(self): self.assertEqual(unicodeify("Li3Fe2(PO4)3"), "Li₃Fe₂(PO₄)₃") self.assertRaises(ValueError, unicodeify, "Li0.2Na0.8Cl")
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