Beispiel #1
0
    def _setup_per_b_fields(self):
        """ Calculate the enthalpy and volume per "B" in A_x B. """
        if self._per_b_done:
            return
        for ind, doc in enumerate(self.cursor):
            nums_b = len(self.species[1:]) * [0]
            for elem in doc['stoichiometry']:
                for chem_pot_ind, chem_pot in enumerate(self.species[1:]):
                    if elem[0] == chem_pot:
                        nums_b[chem_pot_ind] += elem[1]

            num_b = sum(nums_b)
            num_fu = doc['num_fu']
            if num_b == 0:
                self.cursor[ind][self._extensive_energy_key + '_per_b'] = 12345e5
                self.cursor[ind]['cell_volume_per_b'] = 12345e5
            else:
                self.cursor[ind][self._extensive_energy_key + '_per_b'] = doc[self._extensive_energy_key] / (num_b * num_fu)
                if 'cell_volume' in doc:
                    self.cursor[ind]['cell_volume_per_b'] = doc['cell_volume'] / (num_b * num_fu)

        capacities = np.zeros((len(self.cursor)))
        for i, doc in enumerate(self.cursor):
            concs = self.cursor[i]['concentration']
            concs = get_padded_composition(doc['stoichiometry'], self.elements)
            capacities[i] = get_generic_grav_capacity(concs, self.elements)
        set_cursor_from_array(self.cursor, capacities, 'gravimetric_capacity')
        self._per_b_done = True
Beispiel #2
0
    def _calculate_binary_voltage_curve(self, hull_cursor):
        """ Generate binary voltage curve, setting the self.voltage_data
        dictionary.

        Parameters:
            hull_cursor (list(dict)): list of structures to include in the voltage curve.

        """
        mu_enthalpy = get_array_from_cursor(self.chempot_cursor,
                                            self.energy_key)
        # take a copy of the cursor so it can be reordered
        _hull_cursor = deepcopy(hull_cursor)
        x = get_num_intercalated(_hull_cursor)
        _x_order = np.argsort(x)
        capacities = np.asarray([
            get_generic_grav_capacity(
                get_padded_composition(doc['stoichiometry'], self.elements),
                self.elements) for doc in _hull_cursor
        ])
        set_cursor_from_array(_hull_cursor, x, 'conc_of_active_ion')
        set_cursor_from_array(_hull_cursor, capacities, 'gravimetric_capacity')
        _hull_cursor = sorted(_hull_cursor,
                              key=lambda doc: doc['conc_of_active_ion'])
        capacities = capacities[_x_order]
        x = np.sort(x)
        stable_enthalpy_per_b = get_array_from_cursor(
            _hull_cursor, self._extensive_energy_key + '_per_b')

        x, uniq_idxs = np.unique(x, return_index=True)
        stable_enthalpy_per_b = stable_enthalpy_per_b[uniq_idxs]
        capacities = capacities[uniq_idxs]

        V = np.zeros_like(x)
        V[1:] = -np.diff(stable_enthalpy_per_b) / np.diff(x) + mu_enthalpy[0]
        V[0] = V[1]

        reactions = [((None, get_formula_from_stoich(doc['stoichiometry'])), )
                     for doc in _hull_cursor[1:]]

        # make V, Q and x available for plotting, stripping NaNs to re-add later
        # in the edge case of duplicate chemical potentials
        capacities, unique_caps = np.unique(capacities, return_index=True)
        non_nan = np.argwhere(np.isfinite(capacities))
        V = (np.asarray(V)[unique_caps])[non_nan].flatten().tolist()
        x = (np.asarray(x)[unique_caps])[non_nan].flatten().tolist()
        capacities = capacities[non_nan].flatten().tolist()

        x.append(np.nan)
        capacities.append(np.nan)
        V.append(0.0)

        average_voltage = Electrode.calculate_average_voltage(capacities, V)
        profile = VoltageProfile(
            voltages=V,
            capacities=capacities,
            average_voltage=average_voltage,
            starting_stoichiometry=[[self.species[1], 1.0]],
            reactions=reactions,
            active_ion=self.species[0])
        self.voltage_data.append(profile)
Beispiel #3
0
    def test_gravimetric_capacity(self):
        test_docs = []
        test_elements = []
        Q = []

        # Li3P and symmetry test
        doc = dict()
        doc["stoichiometry"] = [["Li", 3], ["P", 1]]
        test_docs.append(doc)
        test_elements.append(["Li", "P"])
        doc = dict()
        doc["stoichiometry"] = [["P", 1], ["Li", 3]]
        test_docs.append(doc)
        test_elements.append(["Li", "P"])
        # ternary test
        doc = dict()
        doc["stoichiometry"] = [["Li", 1], ["Mo", 1], ["S", 2]]
        test_docs.append(doc)
        test_elements.append(["Li", "Mo", "S"])
        doc = dict()
        doc["stoichiometry"] = [["Li", 8], ["Mo", 1], ["S", 2]]
        test_docs.append(doc)
        test_elements.append(["Li", "Mo", "S"])
        doc = dict()
        doc["stoichiometry"] = [["Li", 1], ["Mo", 2], ["S", 4]]
        test_docs.append(doc)
        test_elements.append(["Li", "Mo", "S"])
        for doc, elem in zip(test_docs, test_elements):
            doc["concentration"] = get_concentration(doc, elem)
            temp_conc = list(doc["concentration"])
            temp_conc.append(1.0)
            for conc in doc["concentration"]:
                temp_conc[-1] -= conc

            Q.append(get_generic_grav_capacity(temp_conc, elem))
        self.assertAlmostEqual(Q[0], 2596.096626125, places=3)
        self.assertAlmostEqual(Q[2], 167.449, places=3)
        self.assertEqual(Q[0], Q[1])
        self.assertEqual(round(8 * Q[2], 3), round(Q[3], 3))
        self.assertEqual(round(Q[2], 3), round(2 * Q[4], 3))
Beispiel #4
0
    def _compute_voltages_from_intersections(
            self, intersections, hull, crossover, endstoich, hull_cursor
    ):
        """ Traverse the composition pathway and its intersections with hull facets
        and compute the voltage and volume drops along the way, for a ternary system
        with N 3-phase regions.

        Parameters:
            intersections (np.ndarray): Nx3 array containing the list of face indices
                crossed by the path.
            hull (scipy.spatial.ConvexHull): the temporary hull object that is referred
                to by any indices.
            crossover (np.ndarray): (N+1)x3 array containing the coordinates at which
                the composition pathway crosses the hull faces.
            endstoich (list): a matador stoichiometry that defines the end of the pathway.
            hull_cursor (list): the actual structures used to create the hull.

        """
        if len(crossover) != len(intersections) + 1:
            raise RuntimeError("Incompatible number of crossovers ({}) and intersections ({})."
                               .format(np.shape(crossover), np.shape(intersections)))

        # set up output arrays
        voltages = np.empty(len(intersections) + 1)
        volumes = np.empty(len(intersections))
        reactions = []

        # set up some useful arrays for later
        points = hull.points
        mu_enthalpy = get_array_from_cursor(self.chempot_cursor, self.energy_key)
        if not self._non_elemental:
            input_volumes = get_array_from_cursor(hull_cursor, 'cell_volume') / get_array_from_cursor(hull_cursor, 'num_atoms')
        capacities = [get_generic_grav_capacity(point, self.species) for point in crossover]

        # initial composition of one formula unit of the starting material
        initial_comp = get_padded_composition(endstoich, self.species)

        # loop over intersected faces and compute the voltage and volume at the
        # crossover with that face
        for ind, face in enumerate(intersections):

            simplex_index = int(face[0])

            final_stoichs = [hull_cursor[idx]['stoichiometry'] for idx in hull.simplices[simplex_index]]
            atoms_per_fu = [sum([elem[1] for elem in stoich]) for stoich in final_stoichs]

            energy_vec = points[hull.simplices[simplex_index], 2]
            comp = points[hull.simplices[simplex_index], :]
            comp[:, 2] = 1 - comp[:, 0] - comp[:, 1]

            # normalize the crossover composition to one formula unit of the starting electrode
            norm = np.asarray(crossover[ind][1:]) / np.asarray(initial_comp[1:])
            ratios_of_phases = np.linalg.solve(comp.T, crossover[ind] / norm[0])

            # remove small numerical noise
            ratios_of_phases[np.where(ratios_of_phases < EPS)] = 0

            # create a list containing the sections of the balanced reaction for printing
            balanced_reaction = []
            for i, ratio in enumerate(ratios_of_phases):
                if ratio < EPS:
                    continue
                else:
                    formula = get_formula_from_stoich(final_stoichs[i])
                    balanced_reaction.append((ratio / atoms_per_fu[i], formula))
            reactions.append(balanced_reaction)

            # compute the voltage as the difference between the projection of the gradient down to pure active ion,
            # and the reference chemical potential
            comp_inv = np.linalg.inv(comp.T)
            V = -(comp_inv.dot([1, 0, 0])).dot(energy_vec)
            V = V + mu_enthalpy[0]

            # compute the volume of the final electrode
            if not self._non_elemental:
                if ind <= len(intersections) - 1:
                    volume_vec = input_volumes[hull.simplices[simplex_index]]
                    volumes[ind] = np.dot(ratios_of_phases, volume_vec)

            # double up on first voltage
            if ind == 0:
                voltages[0] = V
            voltages[ind+1] = V

        average_voltage = Electrode.calculate_average_voltage(capacities, voltages)

        # print the reaction over a few lines, remove 1s and rounded ratios
        print(
            ' ---> '.join(
                [' + '.join(
                    ["{}{}".format(
                        str(round(chem[0], 3)) + ' ' if abs(chem[0] - 1) > EPS else '',
                        chem[1])
                     for chem in region])
                 for region in reactions])
        )

        return reactions, capacities, voltages, average_voltage, volumes
Beispiel #5
0
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