Source code for matador.plotting.battery_plotting

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

""" This submodule contains functions to plot battery-relevant curves,
such as voltages and volume expansions.

"""

from typing import List, Optional, Dict, Union

import numpy as np
from matador.utils.chem_utils import (
    get_formula_from_stoich,
    get_subscripted_formula_tex,
)
from matador.plotting.plotting import plotting_function, SAVE_EXTS
from matador.battery import VoltageProfile

EPS = 1e-12


__all__ = ["plot_voltage_curve", "plot_volume_curve"]


[docs]@plotting_function def plot_voltage_curve( profiles: Union[List[VoltageProfile], VoltageProfile], ax=None, show: bool = False, labels: bool = False, savefig: Optional[str] = None, curve_labels: Optional[Union[str, List[str]]] = None, line_kwargs: Optional[Union[Dict, List[Dict]]] = None, shade_average_voltage: bool = False, colour_by_stoichiometry: bool = False, plot_average_voltage: bool = False, plot_max_capacity: bool = False, legend: bool = True, label_suffix: str = None, expt: Optional[str] = None, expt_label: Optional[str] = None, ): """Plot voltage curve calculated for phase diagram. Parameters: profiles (list/VoltageProfile): list of/single voltage profile(s). Keyword arguments: ax (matplotlib.axes.Axes): an existing axis on which to plot. show (bool): whether to show plot in an X window. savefig (str): filename to use to save the plot. curve_labels (list): optional list of labels for the curves in the profiles list. line_kwargs (list or dict): parameters to pass to the curve plotter, if a list then the line kwargs will be passed to each line individually. shade_average_voltage (bool): whether to shade the region spanned from average voltage to max capacity. plot_average_voltage (bool): whether to draw a guideline at the average voltage. plot_max_capacity (bool): whether to draw a guideline at the maximum capacity. legend (bool): whether to draw the legend. label_suffix (str): A suffix to add to every label on the plot (useful when combining axes) colour_by_stoichiometry (bool): whether to use mixed VESTA colours for each curve based on the starting formula of the electrode. expt (str): string to a filename of a csv Q, V to add to the plot. expt_label (str): label for any experimental profile passed to the plot. """ import matplotlib.pyplot as plt if ax is None: fig = plt.figure() ax = fig.add_subplot(111) if not isinstance(profiles, list): profiles = [profiles] if curve_labels is not None and not isinstance(curve_labels, list): curve_labels = [curve_labels] if line_kwargs is not None and not isinstance(line_kwargs, list): line_kwargs = len(profiles) * [line_kwargs] if curve_labels is not None and len(curve_labels) != len(profiles): raise RuntimeError( "Wrong number of labels passed for number of profiles: {} vs {}".format( len(curve_labels), len(profiles) ) ) if line_kwargs is not None and len(line_kwargs) != len(profiles): raise RuntimeError( "Wrong number of line kwargs passed for number of profiles: {} vs {}".format( len(line_kwargs), len(profiles) ) ) dft_label = None if expt is not None: expt_data = np.loadtxt(expt, delimiter=",") if expt_label: ax.plot( expt_data[:, 0], expt_data[:, 1], c="k", lw=2, ls="-", label=expt_label ) else: ax.plot( expt_data[:, 0], expt_data[:, 1], c="k", lw=2, ls="-", label="Experiment", ) if len(profiles) == 1: dft_label = "DFT (this work)" line_colours = list(plt.rcParams["axes.prop_cycle"].by_key()["color"]) for ind, profile in enumerate(profiles): if dft_label is None and curve_labels is None: stoich_label = get_formula_from_stoich( profile.starting_stoichiometry, tex=True ) else: stoich_label = None _label = stoich_label if dft_label is None else dft_label if curve_labels is not None and len(curve_labels) > ind: _label = curve_labels[ind] if label_suffix: _label += f" {label_suffix}" _line_kwargs = {"c": line_colours[(ind % len(line_colours)) + 2]} if line_kwargs is not None: _line_kwargs.update(line_kwargs[ind]) if colour_by_stoichiometry: from matador.utils.viz_utils import formula_to_colour colour = formula_to_colour( get_formula_from_stoich(profile.starting_stoichiometry) ) _line_kwargs["c"] = colour else: colour = _line_kwargs["c"] _add_voltage_curve( profile.capacities, profile.voltages, ax, label=_label, **_line_kwargs ) if shade_average_voltage: ax.fill_between( np.linspace( 0, np.nanmax(profile.capacities), 2, ), np.linspace(0, 0, 2), [profile.average_voltage, profile.average_voltage], lw=1, edgecolor=colour, facecolor=colour + "20", zorder=-ind + 2, ls="--", ) if plot_average_voltage: vline_kwargs = _line_kwargs vline_kwargs["ls"] = "--" vline_kwargs["alpha"] = 0.8 vline_kwargs["zorder"] = 0 ax.axhline(profile.average_voltage, **vline_kwargs) if plot_max_capacity: vline_kwargs = _line_kwargs vline_kwargs["ls"] = "-." vline_kwargs["alpha"] = 0.8 vline_kwargs["zorder"] = 0 ax.axvline(np.nanmax(profile.capacities), **vline_kwargs) if labels: if len(profiles) > 1: print("Only labelling first voltage profile.") for ind, reaction in enumerate(profiles[0].reactions[:-1]): _labels = [] for phase in reaction: if phase[0] is None or phase[0] == 1.0: _label = "" else: _label = "{:.1f} ".format(phase[0]) _label += "{}".format(get_subscripted_formula_tex(phase[1])) _labels.append(_label) _label = "+".join(_labels) _position = ( profiles[0].capacities[ind + 1], profiles[0].voltages[ind + 1] + max(profiles[0].voltages) * 0.01, ) ax.annotate( _label, xy=_position, textcoords="data", ha="center", zorder=9999 ) if expt or len(profiles) > 1: if plot_average_voltage: ax.axvline(-1, c="k", ls="--", alpha=0.8, label="Average voltages") if plot_max_capacity: ax.axvline( -1, c="k", ls="-.", alpha=0.8, label="Maximum gravimetric capacity" ) if legend: ax.legend() ax.set_ylabel("Voltage (V) vs {ion}$^+/${ion}".format(ion=profile.active_ion)) ax.set_xlabel("Gravimetric cap. (mAh/g)") _, end = ax.get_ylim() from matplotlib.ticker import MultipleLocator ax.yaxis.set_major_locator(MultipleLocator(0.2)) ax.set_ylim(0, 1.1 * end) _, end = ax.get_xlim() ax.set_xlim(0, 1.1 * end) ax.grid(False) plt.tight_layout(pad=0.0, h_pad=1.0, w_pad=0.2) if savefig: plt.savefig(savefig) print("Wrote {}".format(savefig)) elif show: plt.show() return ax
def _add_voltage_curve(capacities, voltages, ax, label=None, **kwargs): """Add the voltage curves stored under hull['voltage_data'] to the plot. Parameters: capacities (list): list or numpy array of capacities. voltages (list): list or numpy array of voltages. ax (matplotlib.axes.Axes): an existing axis object on which to plot. Keyword arguments: **kwargs (dict): to pass to matplotlib, using abbreviated names (e.g. 'c' not 'color') label (str): if present, add this label to the first segment of the voltage curve so it can be added to the legend. alpha (float): transparency of line """ line_kwargs = { "lw": 2, "alpha": 1, } line_kwargs.update(kwargs) for i in range(1, len(voltages) - 1): if i == 1 and label is not None: ax.plot( [capacities[i - 1], capacities[i]], [voltages[i], voltages[i]], label=label, **line_kwargs, ) else: ax.plot( [capacities[i - 1], capacities[i]], [voltages[i], voltages[i]], **line_kwargs, ) if i != len(voltages) - 2: ax.plot( [capacities[i], capacities[i]], [voltages[i], voltages[i + 1]], **line_kwargs, )
[docs]@plotting_function def plot_volume_curve( hull, ax=None, show=True, legend=False, as_percentages=False, label=None, label_suffix=None, exclude_elemental=False, line_kwargs: Optional[Union[Dict, List[Dict]]] = None, colour_by_stoichiometry: bool = False, end_marker_only: bool = True, **kwargs, ): """Plot volume curve calculated for phase diagram. 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 plot in X-window. legend (bool): whether to add the legend. label (str/list): Either a list of labels or a single label for the volume curves. label_suffix (str): A suffix to add to every label on the plot (useful when combining axes) as_percentages (bool): whether to show the expansion as a percentage. exclude_elemental (bool): whether to include any elemental electrodes when considering ternary phase diagrams. line_kwargs (list or dict): parameters to pass to the curve plotter, if a list then the line kwargs will be passed to each line individually. colour_by_stoichiometry (bool): whether to use mixed VESTA colours for each curve based on the starting formula of the electrode. end_marker_only (bool): Only place a marker on the worst volume expansion for each curve. """ import matplotlib.pyplot as plt if ax is None: if hull.savefig or any([kwargs.get(ext) for ext in SAVE_EXTS]): fig = plt.figure() else: fig = plt.figure() ax = fig.add_subplot(111) else: ax = ax if label and not isinstance(label, list): label = [label] num_curves = len(hull.volume_data["Q"]) if line_kwargs is not None and not isinstance(line_kwargs, list): line_kwargs = num_curves * [line_kwargs] if as_percentages: volume_key = "volume_expansion_percentage" else: volume_key = "volume_ratio_with_bulk" line_colours = plt.rcParams["axes.prop_cycle"].by_key()["color"] for j in range(num_curves): if exclude_elemental and len(hull.volume_data["endstoichs"][j]) == 1: continue _line_kwargs = {"c": line_colours[(j % len(line_colours)) + 2]} if line_kwargs is not None: _line_kwargs.update(line_kwargs[j]) if colour_by_stoichiometry: from matador.utils.viz_utils import formula_to_colour colour = formula_to_colour( get_formula_from_stoich(hull.volume_data["endstoichs"][j]) ) _line_kwargs["c"] = colour else: colour = _line_kwargs["c"] lw = _line_kwargs.pop("lw", 2) marker = _line_kwargs.pop("marker", "o") markeredgewidth = _line_kwargs.pop("markeredgewidth", 1.5) markeredgecolor = _line_kwargs.pop("markeredgecolor", "k") stable_hull_dist = hull.volume_data["hull_distances"][j] if len(stable_hull_dist) != len(hull.volume_data["Q"][j]): raise RuntimeError("This plot does not support --hull_cutoff.") if end_marker_only: ax.plot( [0, np.max(hull.volume_data["Q"][j])], [1, np.max(hull.volume_data[volume_key][j])], marker=marker, markeredgewidth=markeredgewidth, markeredgecolor=markeredgecolor, zorder=1000, lw=0, **_line_kwargs, ) else: ax.plot( [ q for ind, q in enumerate(hull.volume_data["Q"][j]) if stable_hull_dist[ind] <= EPS ], [ v for ind, v in enumerate(hull.volume_data[volume_key][j]) if stable_hull_dist[ind] <= EPS ], marker=marker, markeredgewidth=markeredgewidth, markeredgecolor=markeredgecolor, zorder=1000, lw=0, **_line_kwargs, ) if label: _label = label[j] else: _label = get_formula_from_stoich( hull.volume_data["endstoichs"][j], tex=True ) if label_suffix: _label += f" {label_suffix}" ax.plot( [ q for ind, q in enumerate(hull.volume_data["Q"][j]) if stable_hull_dist[ind] <= EPS ], [ v for ind, v in enumerate(hull.volume_data[volume_key][j]) if stable_hull_dist[ind] <= EPS ], label=_label, lw=lw, **_line_kwargs, ) ax.set_xlabel("Gravimetric capacity (mAh/g)") if as_percentages: ax.set_ylabel("Volume expansion (%)") else: if len(hull.volume_data["endstoichs"]) == 1: ax.set_ylabel( f"Volume ratio with {get_formula_from_stoich(hull.volume_data['endstoichs'][0], tex=True)} starting electrode" ) else: ax.set_ylabel("Volume ratio with starting electrode") if legend or len(hull.volume_data["Q"]) > 1: ax.legend() return ax