Esempio n. 1
0
        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
Esempio n. 2
0
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)
Esempio n. 3
0
    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
Esempio n. 4
0
    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"""
Esempio n. 5
0
    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)
Esempio n. 6
0
    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
Esempio n. 7
0
    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
Esempio n. 8
0
 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")
Esempio n. 9
0
    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)
Esempio n. 10
0
    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
Esempio n. 11
0
 def test_unicodeify(self):
     self.assertEqual(unicodeify("Li3Fe2(PO4)3"), "Li₃Fe₂(PO₄)₃")
     self.assertRaises(ValueError, unicodeify, "Li0.2Na0.8Cl")
Esempio n. 12
0
 def test_unicodeify(self):
     self.assertEqual(unicodeify("Li3Fe2(PO4)3"),
                      "Li₃Fe₂(PO₄)₃")
     self.assertRaises(ValueError, unicodeify,
                       "Li0.2Na0.8Cl")
Esempio n. 13
0
    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