Source code for matador.plotting.hull_plotting

# coding: utf-8
# Distributed under the terms of the MIT License.

""" This submodule contains functions to plot convex hulls and phase
diagrams generally.

"""


import numpy as np
from matador.utils.chem_utils import get_stoich_from_formula, get_formula_from_stoich
from matador.plotting.plotting import plotting_function, get_linear_cmap, SAVE_EXTS

EPS = 1e-12

__all__ = ['plot_2d_hull', 'plot_ternary_hull', 'plot_ensemble_hull', 'plot_temperature_hull']


def _get_hull_labels(hull, label_cutoff=None, num_species=None, exclude_edges=True):
    """ Return list of structures to labels on phase diagram.

    Parameters:
        hull (matador.hull.QueryConvexHull): phase diagram to plot.

    Keyword arguments:
        num_species (int): structures containing this number of species
            will be labelled.
        exclude_edges (bool): ignore any structure that has zero of any
            chemical potential (i.e the edge of the hull).

    Returns:
        label_cursor (list(dict)): list of matador documents to label.

    """
    eps = 1e-9
    if label_cutoff is None:
        label_cutoff = 0.0
    if isinstance(label_cutoff, list) and len(label_cutoff) == 2:
        label_cutoff = sorted(label_cutoff)
        # first, only apply upper limit as we need to filter by stoich aftewards
        label_cursor = [doc for doc in hull.cursor if doc['hull_distance'] <= label_cutoff[1]]
    else:
        if isinstance(label_cutoff, list):
            assert len(label_cutoff) == 1, 'Incorrect number of label_cutoff values passed, should be 1 or 2.'
            label_cutoff = label_cutoff[0]
        label_cursor = [doc for doc in hull.cursor if doc['hull_distance'] <= label_cutoff + eps]

    num_labels = len({get_formula_from_stoich(doc['stoichiometry']) for doc in label_cursor})
    if num_labels < len(label_cursor):
        tmp_cursor = []
        for doc in label_cursor:
            if doc['stoichiometry'] not in [_doc['stoichiometry'] for _doc in tmp_cursor]:
                tmp_cursor.append(doc)
            else:
                label_cursor = tmp_cursor
    if isinstance(label_cutoff, list) and len(label_cutoff) == 2:
        # now apply lower limit
        label_cursor = [doc for doc in label_cursor if label_cutoff[0] <= doc['hull_distance'] <= label_cutoff[1]]
    # remove chemical potentials and unwanted e.g. binaries
    if num_species is not None:
        label_cursor = [doc for doc in label_cursor if len(doc['stoichiometry']) == num_species]
    if exclude_edges:
        label_cursor = [doc for doc in label_cursor if (all(doc['concentration']) > 0 and
                                                        sum(doc['concentration']) <= 1 - 1e-6)]

    label_cursor = sorted(label_cursor, key=lambda doc: doc['concentration'])

    return label_cursor


[docs]@plotting_function def plot_2d_hull(hull, ax=None, show=True, plot_points=True, plot_tie_line=True, plot_hull_points=True, labels=None, label_cutoff=None, colour_by_source=False, sources=None, hull_label=None, source_labels=None, title=True, plot_fname=None, show_cbar=True, label_offset=(1.15, 0.05), eform_limits=None, legend_kwargs=None, **kwargs): """ Plot calculated hull, returning ax and fig objects for further editing. Parameters: hull (matador.hull.QueryConvexHull): matador hull object. Keyword arguments: ax (matplotlib.axes.Axes): an existing axis on which to plot, show (bool): whether or not to display the plot in an X window, plot_points (bool): whether or not to display off-hull structures, plot_hull_points (bool): whether or not to display on-hull structures, labels (bool): whether to label formulae of hull structures, also read from hull.args. label_cutoff (float/:obj:`tuple` of :obj:`float`): draw labels less than or between these distances form the hull, also read from hull.args. colour_by_source (bool): plot and label points by their sources alpha (float): alpha value of points when colour_by_source is True sources (list): list of possible provenances to colour when colour_by_source is True (others will be grey) title (str/bool): whether to include a plot title. png/pdf/svg (bool): whether or not to write the plot to a file. plot_fname (str): filename to write plot to, without file extension. Returns: matplotlib.axes.Axes: matplotlib axis with plot. """ import matplotlib.pyplot as plt import matplotlib.colors as colours if ax is None: fig = plt.figure() ax = fig.add_subplot(111) if not hasattr(hull, 'colours'): hull.colours = list(plt.rcParams['axes.prop_cycle'].by_key()['color']) hull.default_cmap_list = get_linear_cmap(hull.colours[1:4], list_only=True) hull.default_cmap = get_linear_cmap(hull.colours[1:4], list_only=False) if labels is None: labels = hull.args.get('labels', False) if label_cutoff is None: label_cutoff = hull.args.get('label_cutoff') scale = 1 scatter = [] chempot_labels = [get_formula_from_stoich(get_stoich_from_formula(species, sort=False), tex=True) for species in hull.species] tie_line = hull.convex_hull.points[hull.convex_hull.vertices] # plot hull structures if plot_hull_points: ax.scatter(tie_line[:, 0], tie_line[:, 1], c=hull.colours[1], marker='o', zorder=99999, edgecolor='k', s=scale*40, lw=1.5) if plot_tie_line: ax.plot(np.sort(tie_line[:, 0]), tie_line[np.argsort(tie_line[:, 0]), 1], c=hull.colours[0], zorder=1, label=hull_label, marker='o', markerfacecolor=hull.colours[0], markeredgecolor='k', markeredgewidth=1.5, markersize=np.sqrt(scale*40)) if plot_tie_line: ax.plot(np.sort(tie_line[:, 0]), tie_line[np.argsort(tie_line[:, 0]), 1], c=hull.colours[0], zorder=1, label=hull_label, markersize=0) if hull.hull_cutoff > 0: ax.plot(np.sort(tie_line[:, 0]), tie_line[np.argsort(tie_line[:, 0]), 1] + hull.hull_cutoff, '--', c=hull.colours[1], alpha=0.5, zorder=1, label='') # annotate hull structures if labels or label_cutoff is not None: label_cursor = _get_hull_labels(hull, num_species=2, label_cutoff=label_cutoff) already_labelled = [] for ind, doc in enumerate(label_cursor): formula = get_formula_from_stoich(doc['stoichiometry'], sort=True) if formula not in already_labelled: arrowprops = dict(arrowstyle="-|>", lw=2, alpha=1, zorder=1, shrinkA=2, shrinkB=4) min_comp = tie_line[np.argmin(tie_line[:, 1]), 0] e_f = label_cursor[ind]['formation_' + str(hull.energy_key)] conc = label_cursor[ind]['concentration'][0] if conc < min_comp: position = (0.8 * conc, label_offset[0] * (e_f - label_offset[1])) elif label_cursor[ind]['concentration'][0] == min_comp: position = (conc, label_offset[0] * (e_f - label_offset[1])) else: position = (min(1.1 * conc + 0.15, 0.95), label_offset[0] * (e_f - label_offset[1])) ax.annotate(get_formula_from_stoich(doc['stoichiometry'], latex_sub_style=r'\mathregular', tex=True, elements=hull.species, sort=False), xy=(conc, e_f), xytext=position, textcoords='data', ha='right', va='bottom', arrowprops=arrowprops, zorder=1) already_labelled.append(formula) # points for off hull structures; we either colour by source or by energy if plot_points and not colour_by_source: if hull.hull_cutoff == 0: # if no specified hull cutoff, ignore labels and colour by hull distance cmap = hull.default_cmap if plot_points: scatter = ax.scatter(hull.structures[np.argsort(hull.hull_dist), 0][::-1], hull.structures[np.argsort(hull.hull_dist), -1][::-1], s=scale*40, c=np.sort(hull.hull_dist)[::-1], zorder=10000, cmap=cmap, norm=colours.LogNorm(0.02, 2)) if show_cbar: cbar = plt.colorbar(scatter, aspect=30, pad=0.02, ticks=[0, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28]) cbar.ax.tick_params(length=0) cbar.ax.set_yticklabels([0, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28]) cbar.ax.yaxis.set_ticks_position('right') cbar.ax.set_frame_on(False) cbar.outline.set_visible(False) cbar.set_label('Distance from hull (eV/atom)') elif hull.hull_cutoff != 0: # if specified hull cutoff colour those below c = hull.colours[1] for ind in range(len(hull.structures)): if hull.hull_dist[ind] <= hull.hull_cutoff or hull.hull_cutoff == 0: if plot_points: scatter.append(ax.scatter(hull.structures[ind, 0], hull.structures[ind, 1], s=scale*40, alpha=0.9, c=c, zorder=300)) if plot_points: ax.scatter(hull.structures[1:-1, 0], hull.structures[1:-1, 1], s=scale*30, lw=0, alpha=0.3, c=hull.colours[-2], edgecolor='k', zorder=10) elif colour_by_source: _scatter_plot_by_source( hull, ax, scale, kwargs, sources=sources, source_labels=source_labels, plot_hull_points=plot_hull_points, legend_kwargs=legend_kwargs) if eform_limits is None: eform_limits = (np.min(hull.structures[:, 1]), np.max(hull.structures[:, 1])) lims = (-0.1 if eform_limits[0] >= 0 else 1.4*eform_limits[0], eform_limits[1] if eform_limits[0] >= 0 else 0.1) else: lims = sorted(eform_limits) ax.set_ylim(lims) if isinstance(title, bool) and title: if hull._non_elemental: ax.set_title(r'({d[0]})$_\mathrm{{x}}$({d[1]})$_\mathrm{{1-x}}$'.format(d=chempot_labels)) else: ax.set_title(r'{d[0]}$_\mathrm{{x}}${d[1]}$_\mathrm{{1-x}}$'.format(d=chempot_labels)) elif isinstance(title, str) and title != '': ax.set_title(title) plt.locator_params(nbins=3) if hull._non_elemental: ax.set_xlabel(r'x in ({d[0]})$_\mathrm{{x}}$({d[1]})$_\mathrm{{1-x}}$'.format(d=chempot_labels)) else: ax.set_xlabel(r'x in {d[0]}$_\mathrm{{x}}${d[1]}$_\mathrm{{1-x}}$'.format(d=chempot_labels)) ax.grid(False) ax.set_xlim(-0.05, 1.05) ax.set_xticks([0, 0.25, 0.5, 0.75, 1]) ax.set_xticklabels(ax.get_xticks()) ax.set_ylabel('Formation energy (eV/atom)') if hull.savefig or any([kwargs.get(ext) for ext in SAVE_EXTS]): import os fname = plot_fname or ''.join(hull.species) + '_hull' for ext in SAVE_EXTS: if hull.args.get(ext) or kwargs.get(ext): fname_tmp = fname ind = 0 while os.path.isfile('{}.{}'.format(fname_tmp, ext)): ind += 1 fname_tmp = fname + str(ind) fname = fname_tmp plt.savefig('{}.{}'.format(fname, ext), bbox_inches='tight', transparent=True) print('Wrote {}.{}'.format(fname, ext)) if show: plt.show() return ax
[docs]@plotting_function def plot_ensemble_hull(hull, data_key, ax=None, formation_energy_key='formation_enthalpy_per_atom', plot_points=False, plot_hull_points=True, alpha_scale=0.25, plot_hulls=True, voltages=False, show=True, plot_fname=None, **kwargs): """ Plot and generate an ensemble of hulls. If axis not requested, a histogram of frequency of a particular concentration appearing on the convex hull is also generated as a second axis. Parameters: hull (QueryConvexHull): hull object created with a cursor that contains the data key for all entries in hull.cursor. data_key (str): the key under which ensemble data is stored. Keyword arguments: ax (matplotlib.axes.Axes): matplotlib axis object on which to plot. formation_energy_key (str): the key under which formation energies have been stored. alpha_scale (float): value by which to scale transparency of hulls. plot_points (bool): whether to plot the hull points for each hull in the ensemble. plot_hulls (bool): whether to plot the hull tie-lines for each hull in the ensemble. voltages (bool): compute average voltage and heatmaps. """ import matplotlib.pyplot as plt if ax is None: fig = plt.figure() ax = fig.add_subplot(111) n_hulls = len(hull.phase_diagrams) plot_2d_hull(hull, ax=ax, plot_points=False, plot_hull_points=True, show=False, **kwargs) min_ef = 0 colours_list = list(plt.rcParams['axes.prop_cycle'].by_key()['color']) alpha = min([1, max([1/(alpha_scale*n_hulls), 0.01])]) for ind, _ in enumerate(hull.phase_diagrams): hull_cursor = [doc for doc in hull.cursor if doc[data_key]['hull_distance'][ind] <= 0.0 + EPS] min_ef = np.min([doc[data_key][formation_energy_key][ind] for doc in hull_cursor] + [min_ef]) if plot_hulls: ax.plot([doc['concentration'][0] for doc in hull_cursor], [doc[data_key][formation_energy_key][ind] for doc in hull_cursor], alpha=alpha, c='k', lw=0.5, zorder=0) if plot_hull_points: ax.scatter([doc['concentration'][0] for doc in hull_cursor], [doc[data_key][formation_energy_key][ind] for doc in hull_cursor], alpha=alpha, marker='o', c=colours_list[1], lw=0, zorder=0) if plot_points: ax.scatter([doc['concentration'][0] for doc in hull.cursor], [doc[data_key][formation_energy_key][ind] for doc in hull.cursor], alpha=alpha, marker='o', c='k', s=5, lw=0, zorder=0) ax.set_ylim(1.1*min_ef) if hull.savefig or any(kwargs.get(ext) for ext in SAVE_EXTS): if plot_fname is not None: fname = plot_fname else: fname = ''.join(hull.species) + data_key + '_hull' for ext in SAVE_EXTS: if hull.args.get(ext) or kwargs.get(ext): plt.savefig('{}.{}'.format(fname, ext), bbox_inches='tight', transparent=True) print('Wrote {}.{}'.format(fname, ext))
[docs]@plotting_function def plot_temperature_hull( hull, cmap='plasma', cmap_limits=(0.2, 0.8), ax=None, formation_energy_key='formation_free_energy_per_atom', plot_points=False, show_cbar=True, plot_hull_points=True, alpha_scale=1, lw_scale=1, plot_hulls=True, voltages=False, show=True, plot_fname=None, **kwargs ): """ Plot and generate an ensemble of hulls. If axis not requested, a histogram of frequency of a particular concentration appearing on the convex hull is also generated as a second axis. Parameters: hull (QueryConvexHull): hull object created with a cursor that contains the data key for all entries in hull.cursor. Keyword arguments: ax (matplotlib.axes.Axes): matplotlib axis object on which to plot. formation_energy_key (str): the key under which formation energies have been stored. alpha_scale (float): value by which to scale transparency of hulls. plot_points (bool): whether to plot the hull points for each hull in the ensemble. plot_hulls (bool): whether to plot the hull tie-lines for each hull in the ensemble. voltages (bool): compute average voltage and heatmaps. """ import matplotlib.pyplot as plt from matplotlib.colors import rgb2hex data_key = hull.data_key if ax is None: fig = plt.figure() ax = fig.add_subplot(111) n_hulls = len(hull.phase_diagrams) colours = [ rgb2hex(col) for col in plt.cm.get_cmap(cmap)(np.linspace(*cmap_limits, n_hulls)).tolist() ] min_ef = 0 alpha = alpha_scale # hack the energy key so that labels work _cached_key = hull.energy_key hull.energy_key = hull.chempot_energy_key max_temperature = max(hull.temperatures) # set up initial plot without plotting tie line ax = plot_2d_hull( hull, ax=ax, plot_tie_line=False, plot_points=False, plot_hull_points=False, show=False, **kwargs ) hull.energy_key = _cached_key ax.plot([doc['concentration'][0] for doc in hull.hull_cursor], [doc['formation_' + hull.chempot_energy_key] for doc in hull.hull_cursor], marker='o', alpha=1, c='k', lw=2*lw_scale, ls='--', label='Static', zorder=1e5) ind = 0 hull_cursor = [doc for doc in hull.cursor if doc[data_key]['hull_distance'][ind] <= 0.0 + EPS] ax.plot([doc['concentration'][0] for doc in hull_cursor], [doc[data_key][formation_energy_key][ind] for doc in hull_cursor], marker='o', c=colours[ind], lw=2*lw_scale, markeredgewidth=1.5, markeredgecolor='k', ls='--', zorder=1e5, label='Static + ZPE') # plot remaining temperatures for ind, _ in enumerate(hull.temperatures[1:]): hull_cursor = [doc for doc in hull.cursor if doc[data_key]['hull_distance'][ind] <= 0.0 + EPS] min_ef = np.min([doc[data_key][formation_energy_key][ind] for doc in hull_cursor] + [min_ef]) colour_ind = int(n_hulls * hull.temperatures[ind] / max_temperature) if plot_hulls: ax.plot([doc['concentration'][0] for doc in hull_cursor], [doc[data_key][formation_energy_key][ind] for doc in hull_cursor], alpha=alpha, c=colours[colour_ind], lw=1*lw_scale, zorder=0) if plot_hull_points: ax.scatter([doc['concentration'][0] for doc in hull_cursor], [doc[data_key][formation_energy_key][ind] for doc in hull_cursor], alpha=1, marker='o', c=colours[colour_ind], lw=0.5, edgecolor='k', label="Structures on hull" if ind == n_hulls - 2 else None, zorder=1e4) if plot_points: ax.scatter([doc['concentration'][0] for doc in hull.cursor], [doc[data_key][formation_energy_key][ind] for doc in hull.cursor], label="Structures above hull" if ind == n_hulls - 2 else None, alpha=1, marker='o', edgecolor='w', c=colours[colour_ind], lw=0.5, zorder=1e-3) ax.set_ylim(1.1*min_ef) if show_cbar: import matplotlib.colors mappable = plt.cm.ScalarMappable( cmap=matplotlib.colors.LinearSegmentedColormap.from_list('cut', colours), norm=plt.Normalize(vmin=0, vmax=np.max(hull.temperatures)) ) mappable._A = hull.temperatures cbar = plt.colorbar(mappable, alpha=alpha) cbar.ax.tick_params(length=0) cbar.ax.yaxis.set_ticks_position('right') cbar.ax.set_frame_on(False) cbar.outline.set_visible(False) cbar.set_label('Temperature (K)') if hull.savefig or any(kwargs.get(ext) for ext in SAVE_EXTS): if plot_fname is not None: fname = plot_fname else: fname = ''.join(hull.species) + data_key + '_hull' for ext in SAVE_EXTS: if hull.args.get(ext) or kwargs.get(ext): plt.savefig('{}.{}'.format(fname, ext), bbox_inches='tight', transparent=True) print('Wrote {}.{}'.format(fname, ext)) ax.legend() return ax
[docs]@plotting_function def plot_ternary_hull(hull, axis=None, show=True, plot_points=True, hull_cutoff=None, fig_height=None, label_cutoff=None, label_corners=True, expecting_cbar=True, labels=None, plot_fname=None, hull_dist_unit="meV", efmap=None, sampmap=None, capmap=None, pathways=False, **kwargs): """ Plot calculated ternary hull as a 2D projection. Parameters: hull (matador.hull.QueryConvexHull): matador hull object. Keyword arguments: axis (matplotlib.axes.Axes): matplotlib axis object on which to plot. show (bool): whether or not to show plot in X window. plot_points (bool): whether or not to plot each structure as a point. label_cutoff (float/:obj:`tuple` of :obj:`float`): draw labels less than or between these distances form the hull, also read from hull.args. expecting_cbar (bool): whether or not to space out the plot to preserve aspect ratio if a colourbar is present. labels (bool): whether or not to label on-hull structures label_corners (bool): whether or not to put axis labels on corners or edges. hull_dist_unit (str): either "eV" or "meV", png/pdf/svg (bool): whether or not to write the plot to a file. plot_fname (str): filename to write plot to. efmap (bool): plot heatmap of formation energy, sampmap (bool): plot heatmap showing sampling density, capmap (bool): plot heatmap showing gravimetric capacity. pathways (bool): plot the pathway from the starting electrode to active ion. Returns: matplotlib.axes.Axes: matplotlib axis with plot. """ import ternary import matplotlib.pyplot as plt import matplotlib.colors as colours from matador.utils.chem_utils import get_generic_grav_capacity plt.rcParams['axes.linewidth'] = 0 plt.rcParams['xtick.major.size'] = 0 plt.rcParams['ytick.major.size'] = 0 plt.rcParams['xtick.minor.size'] = 0 plt.rcParams['ytick.minor.size'] = 0 if efmap is None: efmap = hull.args.get('efmap') if sampmap is None: sampmap = hull.args.get('sampmap') if capmap is None: capmap = hull.args.get('capmap') if pathways is None: pathways = hull.args.get('pathways') if labels is None: labels = hull.args.get('labels') if label_cutoff is None: label_cutoff = hull.args.get('label_cutoff') if label_cutoff is None: label_cutoff = 0 else: labels = True if hull_cutoff is None and hull.hull_cutoff is None: hull_cutoff = 0 else: hull_cutoff = hull.hull_cutoff print('Plotting ternary hull...') if capmap or efmap: scale = 100 elif sampmap: scale = 20 else: scale = 1 if axis is not None: fig, ax = ternary.figure(scale=scale, ax=axis) else: fig, ax = ternary.figure(scale=scale) # maintain aspect ratio of triangle if fig_height is None: _user_height = plt.rcParams.get("figure.figsize", (8, 6))[0] else: _user_height = fig_height if capmap or efmap or sampmap: fig.set_size_inches(_user_height, 5/8 * _user_height) elif not expecting_cbar: fig.set_size_inches(_user_height, _user_height) else: fig.set_size_inches(_user_height, 5/6.67 * _user_height) ax.boundary(linewidth=2.0, zorder=99) ax.clear_matplotlib_ticks() chempot_labels = [get_formula_from_stoich( get_stoich_from_formula(species, sort=False), sort=False, tex=True) for species in hull.species ] ax.gridlines(color='black', multiple=scale * 0.1, linewidth=0.5) ticks = [float(val) for val in np.linspace(0, 1, 6)] if label_corners: # remove 0 and 1 ticks when labelling corners ticks = ticks[1:-1] ax.left_corner_label(chempot_labels[2], fontsize='large') ax.right_corner_label(chempot_labels[0], fontsize='large') ax.top_corner_label(chempot_labels[1], fontsize='large', offset=0.16) else: ax.left_axis_label(chempot_labels[2], fontsize='large', offset=0.12) ax.right_axis_label(chempot_labels[1], fontsize='large', offset=0.12) ax.bottom_axis_label(chempot_labels[0], fontsize='large', offset=0.08) ax.set_title('-'.join(['{}'.format(label) for label in chempot_labels]), fontsize='large', y=1.02) ax.ticks(axis='lbr', linewidth=1, offset=0.025, fontsize='small', locations=(scale * np.asarray(ticks)).tolist(), ticks=ticks, tick_formats='%.1f') concs = np.zeros((len(hull.structures), 3)) concs[:, :-1] = hull.structures[:, :-1] for i in range(len(concs)): # set third triangular coordinate concs[i, -1] = 1 - concs[i, 0] - concs[i, 1] stable = concs[np.where(hull.hull_dist <= 0 + EPS)] # sort by hull distances so things are plotting the right order concs = concs[np.argsort(hull.hull_dist)].tolist() hull_dist = np.sort(hull.hull_dist) filtered_concs = [] filtered_hull_dists = [] for ind, conc in enumerate(concs): if conc not in filtered_concs: if hull_dist[ind] <= hull.hull_cutoff or (hull.hull_cutoff == 0 and hull_dist[ind] < 0.1): filtered_concs.append(conc) filtered_hull_dists.append(hull_dist[ind]) if hull.args.get('debug'): print('Trying to plot {} points...'.format(len(filtered_concs))) concs = np.asarray(filtered_concs) hull_dist = np.asarray(filtered_hull_dists) min_cut = 0.0 max_cut = 0.2 if hull_dist_unit.lower() == "mev": hull_dist *= 1000 min_cut *= 1000 max_cut *= 1000 hull.colours = list(plt.rcParams['axes.prop_cycle'].by_key()['color']) hull.default_cmap_list = get_linear_cmap(hull.colours[1:4], list_only=True) hull.default_cmap = get_linear_cmap(hull.colours[1:4], list_only=False) n_colours = len(hull.default_cmap_list) colours_hull = hull.default_cmap_list cmap = hull.default_cmap cmap_full = plt.cm.get_cmap('Pastel2') pastel_cmap = colours.LinearSegmentedColormap.from_list('Pastel2', cmap_full.colors) for plane in hull.convex_hull.planes: plane.append(plane[0]) plane = np.asarray(plane) ax.plot(scale * plane, c=hull.colours[0], lw=1.5, alpha=1, zorder=98) if pathways: for phase in stable: if phase[0] == 0 and phase[1] != 0 and phase[2] != 0: ax.plot([scale * phase, [scale, 0, 0]], c='r', alpha=0.2, lw=6, zorder=99) # add points if plot_points: colours_list = [] colour_metric = hull_dist for i, _ in enumerate(colour_metric): if hull_dist[i] >= max_cut: colours_list.append(n_colours - 1) elif hull_dist[i] <= min_cut: colours_list.append(0) else: colours_list.append(int((n_colours - 1) * (hull_dist[i] / max_cut))) colours_list = np.asarray(colours_list) ax.scatter(scale*stable, marker='o', color=hull.colours[1], edgecolors='black', zorder=9999999, s=150, lw=1.5) ax.scatter(scale*concs, colormap=cmap, colorbar=True, cbarlabel='Distance from hull ({}eV/atom)'.format("m" if hull_dist_unit.lower() == "mev" else ""), c=colour_metric, vmax=max_cut, vmin=min_cut, zorder=1000, s=40, alpha=0) for i, _ in enumerate(concs): ax.scatter( scale * concs[i].reshape(1, 3), color=colours_hull[colours_list[i]], marker='o', zorder=10000 - colours_list[i], s=70 * (1 - float(colours_list[i]) / n_colours) + 15, lw=1, edgecolors='black' ) # add colourmaps if capmap: capacities = dict() from ternary.helpers import simplex_iterator for (i, j, k) in simplex_iterator(scale): capacities[(i, j, k)] = get_generic_grav_capacity([ float(i) / scale, float(j) / scale, float(scale - i - j) / scale ], hull.species) ax.heatmap(capacities, style="hexagonal", cbarlabel='Gravimetric capacity (mAh/g)', vmin=0, vmax=3000, cmap=pastel_cmap) elif efmap: energies = dict() fake_structures = [] from ternary.helpers import simplex_iterator for (i, j, k) in simplex_iterator(scale): fake_structures.append([float(i) / scale, float(j) / scale, 0.0]) fake_structures = np.asarray(fake_structures) plane_energies = hull.get_hull_distances(fake_structures, precompute=False) ind = 0 for (i, j, k) in simplex_iterator(scale): energies[(i, j, k)] = -1 * plane_energies[ind] ind += 1 if isinstance(efmap, str): efmap = efmap else: efmap = 'BuPu_r' ax.heatmap(energies, style="hexagonal", cbarlabel='Formation energy (eV/atom)', vmax=0, cmap=efmap) elif sampmap: sampling = dict() from ternary.helpers import simplex_iterator eps = 1.0 / float(scale) for (i, j, k) in simplex_iterator(scale): sampling[(i, j, k)] = np.size(np.where((concs[:, 0] <= float(i)/scale + eps) * (concs[:, 0] >= float(i)/scale - eps) * (concs[:, 1] <= float(j)/scale + eps) * (concs[:, 1] >= float(j)/scale - eps) * (concs[:, 2] <= float(k)/scale + eps) * (concs[:, 2] >= float(k)/scale - eps))) ax.heatmap(sampling, style="hexagonal", cbarlabel='Number of structures', cmap='afmhot') # add labels if labels: label_cursor = _get_hull_labels(hull, label_cutoff=label_cutoff) if len(label_cursor) == 1: label_coords = [[0.25, 0.5]] else: label_coords = [[0.1+(val-0.5)*0.3, val] for val in np.linspace(0.5, 0.8, int(round(len(label_cursor)/2.)+1))] label_coords += [[0.9-(val-0.5)*0.3, val+0.2] for val in np.linspace(0.5, 0.8, int(round(len(label_cursor)/2.)))] from matador.utils.hull_utils import barycentric2cart for ind, doc in enumerate(label_cursor): conc = np.asarray(doc['concentration'] + [1 - sum(doc['concentration'])]) formula = get_formula_from_stoich( doc['stoichiometry'], sort=False, tex=True, latex_sub_style=r'\mathregular', elements=hull.species ) arrowprops = dict(arrowstyle="-|>", color='k', lw=2, alpha=0.5, zorder=1, shrinkA=2, shrinkB=4) cart = barycentric2cart([doc['concentration'] + [0]])[0][:2] min_dist = 1e20 closest_label = 0 for coord_ind, coord in enumerate(label_coords): dist = np.sqrt((cart[0] - coord[0])**2 + (cart[1] - coord[1])**2) if dist < min_dist: min_dist = dist closest_label = coord_ind ax.annotate(formula, scale*conc, textcoords='data', xytext=[scale*val for val in label_coords[closest_label]], ha='right', va='bottom', arrowprops=arrowprops) del label_coords[closest_label] plt.tight_layout(w_pad=0.2) # important for retaining labels if exporting to PDF # see https://github.com/marcharper/python-ternary/issues/36 ax._redraw_labels() # noqa if hull.savefig: fname = plot_fname or ''.join(hull.species) + '_hull' for ext in SAVE_EXTS: if hull.args.get(ext) or kwargs.get(ext): plt.savefig('{}.{}'.format(fname, ext), bbox_inches='tight', transparent=True) print('Wrote {}.{}'.format(fname, ext)) elif show: print('Showing plot...') plt.show() return ax
def _scatter_plot_by_source(hull, ax, scale, kwargs, sources=None, source_labels=None, plot_hull_points=True, legend_kwargs=None): """ Add scatter points to the hull depending on the guessed provenance of a structure. """ from matador.utils.cursor_utils import get_guess_doc_provenance if sources is None: sources = ['AIRSS', 'GA', 'OQMD', 'SWAPS', 'ICSD', 'DOI', 'SM', 'MP', 'PF', 'Other'] if source_labels is None: source_labels = sources else: assert len(source_labels) == len(sources) # hack: double length of hull colours hull.colours.extend(hull.colours) colour_choices = {source: hull.colours[ind + 1] for ind, source in enumerate(sources)} colours = [] concs = [] energies = [] zorders = [] for doc in hull.cursor: source = get_guess_doc_provenance(doc['source']) if source not in sources: # use grey for undesired sources colours.append(hull.colours[-2]) source = 'Other' if 'Other' not in sources: sources.append('Other') source_labels.append('Other') colour_choices['Other'] = hull.colours[-2] else: colours.append(source) zorders.append(sources.index(source)) concs.append(doc['concentration']) energies.append(doc['formation_{}'.format(hull.energy_key)]) sources_present = set(colours) sources_present = [source for source in sources if source in sources_present] colour_choices = {source: hull.colours[ind + 1] for ind, source in enumerate(sources_present)} colours = [colour_choices[src] for src in colours] alpha = kwargs.get('alpha') if alpha is None: alpha = 0.2 for ind, conc in enumerate(concs): if hull.cursor[ind]['hull_distance'] <= 0 + 1e-9 and not plot_hull_points: ax.scatter(conc, energies[ind], c=colours[ind], alpha=1, s=scale*40, edgecolor='k', zorder=zorders[ind]+1e5, lw=1.5) else: ax.scatter(conc, energies[ind], c=colours[ind], alpha=alpha, s=scale*20, zorder=zorders[ind]+100) for ind, source in enumerate(sources_present): ax.scatter(1e10, 1e10, c=colour_choices[source], label=source_labels[ind], alpha=alpha, lw=1) if legend_kwargs is not None: legend = ax.legend(**legend_kwargs) else: legend = ax.legend(ncol=2) legend.set_zorder(1e20) return ax