Source code for cellpy.utils.plotutils

# -*- coding: utf-8 -*-
"""
Utilities for helping to plot cellpy-data.
"""

import collections
import importlib
import itertools
import logging
import os
import sys
import warnings
from io import StringIO
from pathlib import Path

from cellpy.parameters.internal_settings import (
    get_headers_journal,
    get_headers_normal,
    get_headers_step_table,
    get_headers_summary,
)
from cellpy.utils import helpers

try:
    import matplotlib.pyplot as plt

    plt_available = True
except ImportError:
    plt_available = False

try:
    import holoviews as hv
    from holoviews import opts
    from holoviews.plotting.links import RangeToolLink

    hv_available = True
except ImportError:
    hv_available = False

bokeh_available = importlib.util.find_spec("bokeh") is not None

# logger = logging.getLogger(__name__)
logging.captureWarnings(True)

SYMBOL_DICT = {
    "all": [
        "s",
        "o",
        "v",
        "^",
        "<",
        ">",
        "D",
        "p",
        "*",
        "1",
        "2",
        ".",
        ",",
        "3",
        "4",
        "8",
        "p",
        "d",
        "h",
        "H",
        "+",
        "x",
        "X",
        "|",
        "_",
    ],
    "simple": ["s", "o", "v", "^", "<", ">", "*", "d"],
}

COLOR_DICT = {
    "classic": ["b", "g", "r", "c", "m", "y", "k"],
    "grayscale": ["0.00", "0.40", "0.60", "0.70"],
    "bmh": [
        "#348ABD",
        "#A60628",
        "#7A68A6",
        "#467821",
        "#D55E00",
        "#CC79A7",
        "#56B4E9",
        "#009E73",
        "#F0E442",
        "#0072B2",
    ],
    "dark_background": [
        "#8dd3c7",
        "#feffb3",
        "#bfbbd9",
        "#fa8174",
        "#81b1d2",
        "#fdb462",
        "#b3de69",
        "#bc82bd",
        "#ccebc4",
        "#ffed6f",
    ],
    "ggplot": [
        "#E24A33",
        "#348ABD",
        "#988ED5",
        "#777777",
        "#FBC15E",
        "#8EBA42",
        "#FFB5B8",
    ],
    "fivethirtyeight": ["#30a2da", "#fc4f30", "#e5ae38", "#6d904f", "#8b8b8b"],
    "seaborn-colorblind": [
        "#0072B2",
        "#009E73",
        "#D55E00",
        "#CC79A7",
        "#F0E442",
        "#56B4E9",
    ],
    "seaborn-deep": ["#4C72B0", "#55A868", "#C44E52", "#8172B2", "#CCB974", "#64B5CD"],
    "seaborn-bright": [
        "#003FFF",
        "#03ED3A",
        "#E8000B",
        "#8A2BE2",
        "#FFC400",
        "#00D7FF",
    ],
    "seaborn-muted": ["#4878CF", "#6ACC65", "#D65F5F", "#B47CC7", "#C4AD66", "#77BEDB"],
    "seaborn-pastel": [
        "#92C6FF",
        "#97F0AA",
        "#FF9F9A",
        "#D0BBFF",
        "#FFFEA3",
        "#B0E0E6",
    ],
    "seaborn-dark-palette": [
        "#001C7F",
        "#017517",
        "#8C0900",
        "#7600A1",
        "#B8860B",
        "#006374",
    ],
}

hdr_summary = get_headers_summary()
hdr_raw = get_headers_normal()
hdr_steps = get_headers_step_table()
hdr_journal = get_headers_journal()


def _hv_bokeh_available():
    warnings.warn(
        "This utility function will be removed shortly", category=DeprecationWarning
    )
    if not hv_available:
        print("You need holoviews. But I cannot load it. Aborting...")
        return False
    if not bokeh_available:
        print("You need Bokeh. But I cannot find it. Aborting...")
        return False
    return True


[docs]def find_column(columns, label=None, end="cycle_index"): """find columns based on how the column-name ends. Args: columns: pandas columns label: if not provided, generate, if provided, return as is end: the string to use for searching Returns: column header, label """ warnings.warn( "This utility function will be removed shortly", category=DeprecationWarning ) # TODO @jepe: refactor and use col names directly from HeadersNormal instead hdr = None lab = None for col in columns: if col.endswith(end): hdr = col if label is None: lab = col.replace("_", " ") else: lab = label break return hdr, lab
[docs]def plot_concatenated( dataframe, title="", x=None, y=None, err=None, xlabel=None, ylabel=None, points=True, line=True, errors=True, hover=True, width=800, height=300, journal=None, file_id_level=0, hdr_level=None, axis=1, mean_end="_mean", std_end="_std", cycle_end="cycle_index", legend_title="cell-type", marker_size=None, cmap="default_colors", spread=False, extension="bokeh", edges=False, keys=None, simple=False, **kwargs, ): """Create a holoviews plot of the concatenated summary. This function is still under development. Feel free to contribute. Args: dataframe: the concatenated summary title (str): title of the plot (defaults to empty) x: colum-name for the x variable (not implemented yet) y: colum-name for the y variable (not implemented yet) err: colum-name for the std variable (not implemented yet) xlabel: label for x-axis ylabel: label for y-axis points (bool): plot points if True line (bool): plot line if True errors (bool): plot errors if True hover (bool): add hover tool if True width: width of plot height: height of plot journal: `batch.journal` object file_id_level: the level (multiindex-level) where the cell-names are. hdr_level: the level (multiindex-level) where the parameter names are. axis: what axis to use when looking in the data-frame (row-based or col-based). mean_end: used for searching for y-column names std_end: used for searching for e-column names cycle_end: used for searching for x-column name legend_title: title to put over the legend marker_size: size of the markers used cmap: color-map to use spread (bool): plot error-bands instead of error-bars if True extension (str): "matplotlib" or "bokeh". Note, this uses `hv.extension`) and will affect the state of your notebook edges (bool): show all axes keys (dict): columns to plot (not working yet) simple (bool): making a simple hv.Overlay instead of an hv.NdOverlay if True **kwargs: key-word arguments sent to hv.NdOverlay Example: >>> my_mpl_plot = plot_concatenated( >>> cap_cycle_norm_fast_1000, journal=b.experiment.journal, >>> height=500, marker_size=5, >>> extension="matplotlib", >>> edges=True, >>> ) >>> my_bokeh_plot = plot_concatenated( >>> cap_cycle_norm_fast_1000, journal=b.experiment.journal, >>> height=500, marker_size=5, >>> edges=True, >>> ) Example: >>> # Simple conversion from bokeh to matplotlib >>> # NB! make sure you have only used matplotlib-bokeh convertable key-words (not marker_size) >>> hv.extension("matplotlib") >>> my_plot.opts(aspect="auto", fig_inches=(12,7), fig_size=90, legend_position="top_right", >>> legend_cols = 2, >>> show_frame=True) """ # TODO: add option for using labels from journal in the legend # TODO @jepe: refactor and use col names directly from HeadersNormal instead warnings.warn( "This utility function will be removed shortly", category=DeprecationWarning ) if keys is None: keys = dict() if not hv_available: print( "This function uses holoviews. But could not import it." "So I am aborting..." ) return if extension == "matplotlib": hover = False elif extension == "plotly": print("The plotly backend might not work properly yet.") print("Fingers crossed.") print( "(at least, make sure you are using the most " "recent versions of jupyter, holoviews and plotly)" ) try: current_extension = hv.Store.current_backend if extension != current_extension: hv.extension(extension, logo=False) except Exception as e: hv.extension(extension, logo=False) if hdr_level is None: hdr_level = 0 if file_id_level == 1 else 1 averaged = True columns = list(set(dataframe.columns.get_level_values(hdr_level))) hdr_x, lab_x = find_column(columns, label=xlabel, end=cycle_end) hdr_y, lab_y = find_column(columns, label=ylabel, end=mean_end) if hdr_y is None: averaged = False errors = False if hdr_x is not None: columns.remove(hdr_x) hdr_y = columns[0] if ylabel is None: lab_y = hdr_y.replace("_", " ") else: lab_y = ylabel if errors: hdr_e, _ = find_column(columns, end=std_end) grouped = dataframe.groupby(axis=axis, level=file_id_level) curve_dict = dict() if not averaged and journal is not None: journal_pages = journal.pages[ [hdr_journal["group"], hdr_journal["sub_group"]] ].copy() journal_pages["g"] = 0 journal_pages["sg"] = 0 markers = itertools.cycle(["s", "o", "<", "*", "+", "x"]) colors = itertools.cycle(hv.Cycle(cmap).values) j = journal_pages.groupby(hdr_journal["group"]) for i, (jn, jg) in enumerate(j): journal_pages.loc[journal_pages["group"] == jn, "g"] = i journal_pages.loc[journal_pages["group"] == jn, "sg"] = list(range(len(jg))) markers = [next(markers) for _ in range(journal_pages["sg"].max() + 1)] colors = [next(colors) for _ in range(journal_pages["g"].max() + 1)] journal_pages = journal_pages[["g", "sg"]] for i, (name, group) in enumerate(grouped): if name in keys: label = keys[name] else: label = name keys[name] = name group.columns = group.columns.droplevel(file_id_level) if hdr_x is None: group = group.reset_index() hdr_x = group.columns[0] if lab_x is None: lab_x = hdr_x.replace("_", " ") if not averaged and journal is not None: g = journal_pages.loc[name, "g"] sg = journal_pages.loc[name, "sg"] color = colors[g] marker = markers[sg] curve = hv.Curve(group, (hdr_x, lab_x), (hdr_y, lab_y), label=label).opts( color=color ) else: curve = hv.Curve(group, (hdr_x, lab_x), (hdr_y, lab_y), label=label) if points: if not averaged and journal is not None: scatter = hv.Scatter(curve).opts(color=color, marker=marker) if edges and extension == "matplotlib": scatter = scatter.opts(edgecolor="k") if edges and extension == "bokeh": scatter = scatter.opts(line_color="k", line_width=1) if marker_size is not None and extension == "bokeh": scatter = scatter.opts(size=marker_size) else: scatter = hv.Scatter(curve) if marker_size is not None and extension == "bokeh": scatter = scatter.opts(size=marker_size) if points and line: curve *= scatter elif points: curve = scatter if errors: if spread: curve *= hv.Spread(group, hdr_x, [hdr_y, hdr_e]) else: curve *= hv.ErrorBars( group, hdr_x, [hdr_y, hdr_e] ) # should get the color from curve and set it here curve_dict[label] = curve if extension == "matplotlib": overlay_opts = { "aspect": "auto", "fig_inches": (width * 0.016, height * 0.012), "show_frame": True, } else: overlay_opts = {"width": width, "height": height} if simple: if len(keys) == len(curve_dict): new_curve_dict = {} for k in keys: new_curve_dict[k] = curve_dict[keys[k]] curve_dict = new_curve_dict final_plot = hv.Overlay( [*curve_dict.values()], vdims=[*curve_dict.keys()] ).opts(**overlay_opts, **kwargs) else: overlay_opts["title"] = title logging.info(f"overlay_opts: {overlay_opts}") logging.info(f"additional_kwargs_overlay_opts: {kwargs}") final_plot = hv.NdOverlay(curve_dict, kdims=legend_title).opts( **overlay_opts, **kwargs ) if hover and not extension == "plotly": if points: final_plot.opts(opts.Scatter(tools=["hover"])) else: final_plot.opts(opts.Curve(tools=["hover"])) return final_plot
[docs]def create_colormarkerlist_for_journal( journal, symbol_label="all", color_style_label="seaborn-colorblind" ): """Fetch lists with color names and marker types of correct length for a journal. Args: journal: cellpy journal symbol_label: sub-set of markers to use color_style_label: cmap to use for colors Returns: colors (list), markers (list) """ logging.debug("symbol_label: " + symbol_label) logging.debug("color_style_label: " + color_style_label) groups = journal.pages[hdr_journal.group].unique() sub_groups = journal.pages[hdr_journal.subgroup].unique() return create_colormarkerlist(groups, sub_groups, symbol_label, color_style_label)
[docs]def create_colormarkerlist( groups, sub_groups, symbol_label="all", color_style_label="seaborn-colorblind" ): """Fetch lists with color names and marker types of correct length. Args: groups: list of group numbers (used to generate the list of colors) sub_groups: list of sub-group numbers (used to generate the list of markers). symbol_label: sub-set of markers to use color_style_label: cmap to use for colors Returns: colors (list), markers (list) """ symbol_list = SYMBOL_DICT[symbol_label] color_list = COLOR_DICT[color_style_label] # checking that we have enough colors and symbols (if not, then use cycler (e.g. reset)) color_cycler = itertools.cycle(color_list) symbol_cycler = itertools.cycle(symbol_list) _color_list = [] _symbol_list = [] for i in groups: _color_list.append(next(color_cycler)) for i in sub_groups: _symbol_list.append(next(symbol_cycler)) return _color_list, _symbol_list
def _raw_plot(raw_curve, title="Voltage versus time", **kwargs): tgt = raw_curve.relabel(title).opts( width=800, height=300, labelled=["y"], # tools=["pan","box_zoom", "reset"], active_tools=["pan"], ) src = raw_curve.opts(width=800, height=100, yaxis=None, default_tools=[]) RangeToolLink(src, tgt) layout = (tgt + src).cols(1) layout.opts(opts.Layout(shared_axes=False, merge_tools=False)) return layout
[docs]def raw_plot(cell, y=("voltage", "Voltage (V vs Li/Li+)"), title=None, **kwargs): # TODO: missing doc-string warnings.warn( "This utility function will be replaced shortly", category=DeprecationWarning ) if title is None: if isinstance(y, (list, tuple)): pre_title = str(y[0]) else: pre_title = str(y) title = " ".join([pre_title, "versus", "time"]) if not _hv_bokeh_available(): return hv.extension("bokeh", logo=False) raw = cell.data.raw raw["test_time_hrs"] = raw[hdr_raw["test_time_txt"]] / 3600 x = ("test_time_hrs", "Time (hours)") raw_curve = hv.Curve(raw, x, y) layout = _raw_plot(raw_curve, title=title, **kwargs) return layout
[docs]def cycle_info_plot( cell, cycle=None, step=None, title=None, points=False, x=None, y=None, info_level=1, h_cycle=None, h_step=None, show_it=True, label_cycles=True, label_steps=False, get_axes=False, use_bokeh=True, **kwargs, ): """Show raw data together with step and cycle information. Args: cell: cellpy object cycle: cycles to select (optional, default is all) step: steps to select (optional, defaults is all) title: a title to give the plot points: overlay a scatter plot x (str): column header for the x-value (defaults to "Test_Time") y (str): column header for the y-value (defaults to "Voltage") info_level (int): how much information to display (defaults to 1) (0 - almost nothing 1 - pretty much 2 - something else 3 - not implemented yet). h_cycle: column header for the cycle number (defaults to "Cycle_Index") h_step: column header for the step number (defaults to "Step_Index") show_it (bool): show the figure (defaults to True). If not, return the figure. label_cycles (bool): add labels with cycle numbers. label_steps (bool): add labels with step numbers get_axes (bool): return axes (for matplotlib) use_bokeh (bool): use bokeh to plot (defaults to True). If not, use matplotlib. **kwargs: parameters specific to either matplotlib or bokeh. Returns: ``matplotlib.axes`` or None """ # TODO: missing doc-string warnings.warn( "This utility function will be replaced shortly", category=DeprecationWarning ) if use_bokeh and not bokeh_available: print("OBS! bokeh is not available - using matplotlib instead") use_bokeh = False if use_bokeh: axes = _cycle_info_plot_bokeh( cell, cycle=cycle, step=step, title=title, points=points, x=x, y=y, info_level=info_level, h_cycle=h_cycle, h_step=h_step, show_it=show_it, label_cycles=label_cycles, label_steps=label_steps, **kwargs, ) else: if isinstance(cycle, (list, tuple)): if len(cycle) > 1: print("OBS! The matplotlib-plotter only accepts single cycles.") print(f"Selecting first cycle ({cycle[0]})") cycle = cycle[0] axes = _cycle_info_plot_matplotlib(cell, cycle, get_axes) if get_axes: return axes
def _plot_step(ax, x, y, color): ax.plot(x, y, color=color, linewidth=3) def _get_info(table, cycle, step): # obs! hard-coded col-names. Please fix me. m_table = (table.cycle == cycle) & (table.step == step) p1, p2 = table.loc[m_table, ["point_min", "point_max"]].values[0] c1, c2 = table.loc[m_table, ["current_min", "current_max"]].abs().values[0] d_voltage, d_current = table.loc[ m_table, ["voltage_delta", "current_delta"] ].values[0] d_discharge, d_charge = table.loc[ m_table, ["discharge_delta", "charge_delta"] ].values[0] current_max = (c1 + c2) / 2 rate = table.loc[m_table, "rate_avr"].values[0] step_type = table.loc[m_table, "type"].values[0] return [step_type, rate, current_max, d_voltage, d_current, d_discharge, d_charge] def _add_step_info_cols(df, table, cycles=None, steps=None, h_cycle=None, h_step=None): if h_cycle is None: h_cycle = "cycle_index" # edit if h_step is None: h_step = "step_index" # edit col_name_mapper = {"cycle": h_cycle, "step": h_step} df = df.merge( table.rename(columns=col_name_mapper), on=("cycle_index", "step_index"), how="left", ) return df def _cycle_info_plot_bokeh( cell, cycle=None, step=None, title=None, points=False, x=None, y=None, info_level=0, h_cycle=None, h_step=None, show_it=False, label_cycles=True, label_steps=False, **kwargs, ): """Plot raw data with annotations. This function uses Bokeh for plotting and is intended for use in Jupyter Notebooks. """ # TODO: check that correct new column-names are used # TODO: fix bokeh import (use e.g. import bokeh.io) try: from bokeh.io import output_notebook, show from bokeh.layouts import column, row from bokeh.models import ColumnDataSource, HoverTool, LabelSet from bokeh.models.annotations import Span from bokeh.models.widgets import Slider, TextInput from bokeh.plotting import figure except ImportError: warnings.warn("Could not import bokeh") return try: output_notebook(hide_banner=True) finally: sys.stdout = sys.__stdout__ if points: if cycle is None or (len(cycle) > 1): print("Plotting points only allowed when plotting one single cycle.") print("Turning points off.") points = False if h_cycle is None: h_cycle = "cycle_index" # edit if h_step is None: h_step = "step_index" # edit if x is None: x = "test_time" # edit if y is None: y = "voltage" # edit if isinstance(x, tuple): x, x_label = x else: x_label = x if isinstance(y, tuple): y, y_label = y else: y_label = y t_x = x # used in generating title - replace with a selector t_y = y # used in generating title - replace with a selector if title is None: title = f"{t_y} vs. {t_x}" cols = [x, y] cols.extend([h_cycle, h_step]) df = cell.data.raw.loc[:, cols] if cycle is not None: if not isinstance(cycle, (list, tuple)): cycle = [cycle] _df = df.loc[df[h_cycle].isin(cycle), :] if len(cycle) < 5: title += f" [c:{cycle}]" else: title += f" [c:{cycle[0]}..{cycle[-1]}]" if _df.empty: print(f"EMPTY (available cycles: {df[h_step].unique()})") return else: df = _df cycle = df[h_cycle].unique() if step is not None: if not isinstance(step, (list, tuple)): step = [step] _df = df.loc[df[h_step].isin(step), :] if len(step) < 5: title += f" (s:{step})" else: title += f" [s:{step[0]}..{step[-1]}]" if _df.empty: print(f"EMPTY (available steps: {df[h_step].unique()})") return else: df = _df x_min, x_max = df[x].min(), df[x].max() y_min, y_max = df[y].min(), df[y].max() if info_level > 0: table = cell.data.steps df = _add_step_info_cols(df, table, cycle, step) source = ColumnDataSource(df) plot = figure( title=title, tools="pan,reset,save,wheel_zoom,box_zoom,undo,redo", x_range=[x_min, x_max], y_range=[y_min, y_max], **kwargs, ) plot.line(x, y, source=source, line_width=3, line_alpha=0.6) # labelling cycles if label_cycles: cycle_line_positions = [df.loc[df[h_cycle] == c, x].min() for c in cycle] cycle_line_positions.append(df.loc[df[h_cycle] == cycle[-1], x].max()) for m in cycle_line_positions: _s = Span( location=m, dimension="height", line_color="red", line_width=3, line_alpha=0.5, ) plot.add_layout(_s) s_y_pos = y_min + 0.9 * (y_max - y_min) s_x = [] s_y = [] s_l = [] for s in cycle: s_x_min = df.loc[df[h_cycle] == s, x].min() s_x_max = df.loc[df[h_cycle] == s, x].max() s_x_pos = (s_x_min + s_x_max) / 2 s_x.append(s_x_pos) s_y.append(s_y_pos) s_l.append(f"c{s}") c_labels = ColumnDataSource(data={x: s_x, y: s_y, "names": s_l}) c_labels = LabelSet( x=x, y=y, text="names", level="glyph", source=c_labels, render_mode="canvas", text_color="red", text_alpha=0.7, ) plot.add_layout(c_labels) # labelling steps if label_steps: for c in cycle: step = df.loc[df[h_cycle] == c, h_step].unique() step_line_positions = [ df.loc[(df[h_step] == s) & (df[h_cycle] == c), x].min() for s in step[0:] ] for m in step_line_positions: _s = Span( location=m, dimension="height", line_color="olive", line_width=3, line_alpha=0.1, ) plot.add_layout(_s) # s_y_pos = y_min + 0.8 * (y_max - y_min) s_x = [] s_y = [] s_l = [] for s in step: s_x_min = df.loc[(df[h_step] == s) & (df[h_cycle] == c), x].min() s_x_max = df.loc[(df[h_step] == s) & (df[h_cycle] == c), x].max() s_x_pos = s_x_min s_y_min = df.loc[(df[h_step] == s) & (df[h_cycle] == c), y].min() s_y_max = df.loc[(df[h_step] == s) & (df[h_cycle] == c), y].max() s_y_pos = (s_y_max + s_y_min) / 2 s_x.append(s_x_pos) s_y.append(s_y_pos) s_l.append(f"s{s}") s_labels = ColumnDataSource(data={x: s_x, y: s_y, "names": s_l}) s_labels = LabelSet( x=x, y=y, text="names", level="glyph", source=s_labels, render_mode="canvas", text_color="olive", text_alpha=0.3, ) plot.add_layout(s_labels) hover = HoverTool() if info_level == 0: hover.tooltips = [ (x, "$x{0.2f}"), (y, "$y"), ("cycle", f"@{h_cycle}"), ("step", f"@{h_step}"), ] elif info_level == 1: # insert C-rates etc here hover.tooltips = [ (f"(x,y)", "($x{0.2f} $y"), ("cycle", f"@{h_cycle}"), ("step", f"@{h_step}"), ("step_type", "@type"), ("rate", "@rate_avr{0.2f}"), ] elif info_level == 2: hover.tooltips = [ (x, "$x{0.2f}"), (y, "$y"), ("cycle", f"@{h_cycle}"), ("step", f"@{h_step}"), ("step_type", "@type"), ("rate (C)", "@rate_avr{0.2f}"), ("dv (%)", "@voltage_delta{0.2f}"), ("I-max (A)", "@current_max"), ("I-min (A)", "@current_min"), ("dCharge (%)", "@charge_delta{0.2f}"), ("dDischarge (%)", "@discharge_delta{0.2f}"), ] hover.mode = "vline" plot.add_tools(hover) plot.xaxis.axis_label = x_label plot.yaxis.axis_label = y_label if points: plot.scatter(x, y, source=source, alpha=0.3) if show_it: show(plot) return plot def _cycle_info_plot_matplotlib(cell, cycle, get_axes=False): # obs! hard-coded col-names. Please fix me. if not plt_available: print( "This function uses matplotlib. But I could not import it. " "So I decided to abort..." ) return data = cell.data.raw table = cell.data.steps span_colors = ["#4682B4", "#FFA07A"] voltage_color = "#008B8B" current_color = "#CD5C5C" m_cycle_data = data.cycle_index == cycle all_steps = data[m_cycle_data]["step_index"].unique() color = itertools.cycle(span_colors) fig = plt.figure(figsize=(20, 8)) fig.suptitle(f"Cycle: {cycle}") ax3 = plt.subplot2grid((8, 3), (0, 0), colspan=3, rowspan=1, fig=fig) # steps ax4 = plt.subplot2grid((8, 3), (1, 0), colspan=3, rowspan=2, fig=fig) # rate ax1 = plt.subplot2grid((8, 3), (3, 0), colspan=3, rowspan=5, fig=fig) # data ax2 = ax1.twinx() ax1.set_xlabel("time (minutes)") ax1.set_ylabel("voltage (V vs. Li/Li+)", color=voltage_color) ax2.set_ylabel("current (mA)", color=current_color) annotations_1 = [] # step number (IR) annotations_2 = [] # step number annotations_4 = [] # rate for i, s in enumerate(all_steps): m = m_cycle_data & (data.step_index == s) c = data.loc[m, "current"] * 1000 v = data.loc[m, "voltage"] t = data.loc[m, "test_time"] / 60 step_type, rate, current_max, dv, dc, d_discharge, d_charge = _get_info( table, cycle, s ) if len(t) > 1: fcolor = next(color) info_txt = ( f"{step_type}\nc-rate = {rate}\ni = |{1000 * current_max:0.2f}| mA\n" ) info_txt += f"delta V = {dv:0.2f} %\ndelta i = {dc:0.2f} %\n" info_txt += f"delta C = {d_charge:0.2} %\ndelta DC = {d_discharge:0.2} %\n" for ax in [ax2, ax3, ax4]: ax.axvspan(t.iloc[0], t.iloc[-1], facecolor=fcolor, alpha=0.2) _plot_step(ax1, t, v, voltage_color) _plot_step(ax2, t, c, current_color) annotations_1.append([f"{s}", t.mean()]) annotations_4.append([info_txt, t.mean()]) else: info_txt = f"{s}({step_type})" annotations_2.append([info_txt, t.mean()]) ax3.set_ylim(0, 1) for s in annotations_1: ax3.annotate(f"{s[0]}", (s[1], 0.2), ha="center") for s in annotations_2: ax3.annotate(f"{s[0]}", (s[1], 0.6), ha="center") for s in annotations_4: ax4.annotate(f"{s[0]}", (s[1], 0.0), ha="center") for ax in [ax3, ax4]: ax.axes.get_yaxis().set_visible(False) ax.axes.get_xaxis().set_visible(False) if get_axes: return ax1, ax2, ax2, ax4
[docs]def save_fig(figure, file_name=None, wide=False, size=None, dpi=300, **kwargs): """Save a figure, either a holoviews plot or a matplotlib figure. This function should mainly be used when using the standard cellpy notebook template (generated by '> cellpy new') Args: figure (obj): the figure or plot object file_name (str): the file name wide (bool): release the equal aspect lock (default on for holoviews) size (int or tuple of ints): figure size in inches dpi (int): resolution **kwargs: sent to cellpy.utils.plotutils.hv_bokeh_to_mpl """ warnings.warn( "This utility function will be removed shortly", category=DeprecationWarning ) out_path = Path("out/") extension = "png" if size is None: size = (6, 6) # create file name: if file_name is None: counter = 1 while True: _file_name = f"cellpy-plot-{str(counter).zfill(3)}.{extension}" _file_name = out_path / _file_name if not os.path.isfile(_file_name): break counter += 1 file_name = _file_name type_str = str(type(figure)) is_hv = type_str.find("holoviews") >= 0 is_mpl = type_str.find("matplotlib") >= 0 if is_mpl: is_mpl_figure = type_str.find("figure.Figure") >= 0 is_mpl_axes = type_str.find(".axes.") >= 0 if not is_mpl_figure: if is_mpl_axes: figure = figure.get_figure() else: print("this matplotlib object is not supported") print(type_str) return figure.savefig(file_name, dpi=dpi) elif is_hv: is_hv_nd_overlay = isinstance(figure, hv.core.overlay.NdOverlay) is_hv_overlay = isinstance(figure, hv.core.overlay.Overlay) is_hv_layout = isinstance(figure, hv.core.layout.Layout) figure = hv_bokeh_to_mpl(figure, wide=wide, size=size, **kwargs) figure.savefig(file_name, dpi=dpi) else: print("this figure object is not supported") print(type_str) return print(f"saved to {file_name}")
[docs]def hv_bokeh_to_mpl(figure, wide=False, size=(6, 4), **kwargs): # I think this needs to be tackled differently. For example by setting hv.extension("matplotlib") and # re-making the figure. Or making a custom renderer. warnings.warn( "This utility function will be removed shortly", category=DeprecationWarning ) figure = hv.render(figure, backend="matplotlib") axes = figure.axes number_of_axes = len(axes) if number_of_axes > 1: for j, ax in enumerate(axes): if j < number_of_axes - 1: ax.set_xlabel("") if j < number_of_axes - 1: ax.get_legend().remove() else: handles, labels = ax.get_legend_handles_labels() # This does not work: # ax.legend(handles, labels, bbox_to_anchor=(1.05, 1), loc="upper_right") # TODO: create new legend based on the ax data if wide: for axis in axes: axis.set_aspect("auto") figure.tight_layout() figure.set_size_inches(size) return figure
[docs]def oplot( b, cap_ylim=None, ce_ylim=None, ir_ylim=None, simple=False, group_it=False, spread=True, capacity_unit="gravimetric", capacity_unit_label=None, internal_resistance_unit="Ohm", **kwargs, ): """create a holoviews-plot containing Coulombic Efficiency, Capacity, and IR. Args: b (cellpy.batch object): the batch with the cells. cap_ylim (tuple of two floats): scaling of y-axis for capacity plots. ce_ylim (tuple of two floats): scaling of y-axis for c.e. plots. ir_ylim (tuple of two floats): scaling of y-axis for i.r. plots. simple (bool): if True, use hv.Overlay instead of hv.NdOverlay. group_it (bool): if True, average pr group. spread (bool): if True, show spread instead of error-bars. capacity_unit (str): select "gravimetric", or "areal". capacity_unit_label (str): shown in the plot title for the capacity plot (defaults to mAh/g(a.m.) for gravimetric and mAh/cm2 for areal). internal_resistance_unit (str): shown in the plot title for the ir plots (defaults to Ohm). **kwargs: Sent to plotutils.bplot. Returns: ``hv.Overlay`` or ``hv.NdOverlay`` """ warnings.warn( "This utility function will be removed shortly", category=DeprecationWarning ) extension = kwargs.pop("extension", "bokeh") cap_colum_dict = { "gravimetric": { "discharge": "discharge_capacity_gravimetric", "charge": "charge_capacity_gravimetric", "unit": capacity_unit_label or "mAh/g(a.m.)", "ylim": (0, 5000), }, "areal": { "discharge": "discharge_capacity_areal", "charge": "charge_capacity_areal", "unit": capacity_unit_label or "mAh/cm2", "ylim": (0, 3), }, } if cap_ylim is not None: cap_colum_dict[capacity_unit]["ylim"] = cap_ylim if ce_ylim is None: ce_ylim = (80, 120) if ir_ylim is None: ir_ylim = (0, 200) overlay_sensitive_opts = {"ce": {}, "dcap": {}, "ccap": {}, "ird": {}, "irc": {}} layout_sensitive_opts = {"ce": {}, "dcap": {}, "ccap": {}, "ird": {}, "irc": {}} if extension == "bokeh": overlay_sensitive_opts["ce"] = {"height": 150} overlay_sensitive_opts["dcap"] = {"height": 400} overlay_sensitive_opts["ccap"] = {"height": 150} overlay_sensitive_opts["ird"] = {"height": 150} overlay_sensitive_opts["irc"] = {"height": 150} elif extension == "matplotlib": simple = True overlay_sensitive_opts["ce"] = {"aspect": 2} overlay_sensitive_opts["dcap"] = {"aspect": 2} overlay_sensitive_opts["ccap"] = {"aspect": 2} overlay_sensitive_opts["ird"] = {"aspect": 2} overlay_sensitive_opts["irc"] = {"aspect": 2} hspace = 2 layout_sensitive_opts["ce"] = {"hspace": hspace} layout_sensitive_opts["dcap"] = {"hspace": hspace} layout_sensitive_opts["ccap"] = {"hspace": hspace} layout_sensitive_opts["ird"] = {"hspace": hspace} layout_sensitive_opts["irc"] = {"hspace": hspace} bplot_shared_opts = { "group_it": group_it, "simple": simple, "spread": spread, "extension": extension, } if simple: overlay_opts = hv.opts.Overlay layout_opts = hv.opts.Layout else: overlay_opts = hv.opts.NdOverlay layout_opts = hv.opts.NdLayout # print("creating interactive plots") oplot_ce = bplot( b, columns=["coulombic_efficiency"], **bplot_shared_opts, **kwargs ).opts( hv.opts.Curve(ylim=ce_ylim), overlay_opts( title="", show_legend=False, xlabel="", ylabel="C.E.", **overlay_sensitive_opts["ce"], ), layout_opts(title="Coulombic efficiency (%)", **layout_sensitive_opts["ce"]), ) # print(" - created oplot_ce") oplot_dcap = bplot( b, columns=[cap_colum_dict[capacity_unit]["discharge"]], **bplot_shared_opts, **kwargs, ).opts( hv.opts.Curve(ylim=cap_colum_dict[capacity_unit]["ylim"]), overlay_opts( title="", show_legend=True, xlabel="", ylabel="discharge", **overlay_sensitive_opts["dcap"], ), layout_opts( title=f"Capacity ({cap_colum_dict[capacity_unit]['unit']})", **layout_sensitive_opts["dcap"], ), ) # print(" - created oplot_dcap") oplot_ccap = bplot( b, columns=[cap_colum_dict[capacity_unit]["charge"]], **bplot_shared_opts, **kwargs, ).opts( hv.opts.Curve(ylim=cap_colum_dict[capacity_unit]["ylim"]), overlay_opts( title="", show_legend=False, xlabel="", ylabel="charge", **overlay_sensitive_opts["ccap"], ), layout_opts(title="", **layout_sensitive_opts["ccap"]), ) # print(" - created oplot_ccap") oplot_ird = bplot(b, columns=["ir_discharge"], **bplot_shared_opts, **kwargs).opts( hv.opts.Curve(ylim=ir_ylim), overlay_opts( title="", show_legend=False, xlabel="", ylabel="discharge", **overlay_sensitive_opts["ird"], ), layout_opts( title=f"Internal Resistance ({internal_resistance_unit})", **layout_sensitive_opts["ird"], ), ) oplot_irc = bplot(b, columns=["ir_charge"], **bplot_shared_opts, **kwargs).opts( hv.opts.Curve(ylim=ir_ylim), overlay_opts( title="", show_legend=False, ylabel="charge", **overlay_sensitive_opts["irc"], ), layout_opts(title="", **layout_sensitive_opts["irc"]), ) return (oplot_ce + oplot_dcap + oplot_ccap + oplot_ird + oplot_irc).cols(1)
[docs]def bplot(b, individual=False, cols=1, **kwargs): """plot batch summaries. This is wrapper around the two functions concatenate_summaries and plot_concatenated. >>> p1 = bplot(b, columns=["charge_capacity_gravimetric"], journal=b.experiment.journal, group_it=True) is equivalent to: >>> cs = helpers.concatenate_summaries(b, columns=["charge_capacity_gravimetric"], group_it=True) >>> p1 = plot_concatenated(cs, journal=journal) Args: b (cellpy.batch object): the batch with the cells. individual (bool): in case of multiple columns, return a list of plots instaed of a hv.Layout cols (int): number of columns. Key-word arguments sent further to the concatenator: Keyword Args: rate (float): filter on rate (C-rate) on (str or list of str): only select cycles if based on the rate of this step-type (e.g. on="charge"). columns (list): selected column(s) (using cellpy attribute name) [defaults to "charge_capacity_gravimetric"] column_names (list): selected column(s) (using exact column name) normalize_capacity_on (list): list of cycle numbers that will be used for setting the basis of the normalization (typically the first few cycles after formation) scale_by (float or str): scale the normalized data with nominal capacity if "nom_cap", or given value (defaults to one). nom_cap (float): nominal capacity of the cell normalize_cycles (bool): perform a normalisation of the cycle numbers (also called equivalent cycle index) add_areal (bool): add areal capacity to the summary group_it (bool): if True, average pr group. rate_std (float): allow for this inaccuracy when selecting cycles based on rate rate_column (str): name of the column containing the C-rates. inverse (bool): select steps that do not have the given C-rate. inverted (bool): select cycles that do not have the steps filtered by given C-rate. journal (batch.journal object): the journal (will use the journal in b if not given). Key-word arguments sent further to the plotter: Keyword Args: width (int): width of plot. spread (bool): use error-spread instead of error-bars. simple (bool): use ``hv.Overlay`` instead of ``hv.NdOverlay``. extension (str): plotting backend. Returns: ``holoviews`` plot """ warnings.warn( "This utility function will be removed shortly", category=DeprecationWarning ) width = kwargs.pop("width", 800) journal = kwargs.pop("journal", b.experiment.journal) spread = kwargs.pop("spread", True) simple = kwargs.pop("simple", False) columns = kwargs.pop("columns", ["charge_capacity_gravimetric"]) extension = kwargs.pop("extension", "bokeh") if extension != "bokeh": logging.critical( f"Setting extension to {extension}. Remark that this will globally change the hv settings." ) p = collections.OrderedDict() i_width = width // cols for i, col in enumerate(columns): try: cs = helpers.concatenate_summaries(b, columns=[col], **kwargs) _p = plot_concatenated( cs, journal=journal, spread=spread, width=i_width, extension=extension, title=col, simple=simple, ) if i < len(columns) - 1: _p.opts(show_legend=False) if cols == 1: _p.opts(xlabel="") else: _p.opts(show_legend=True, legend_position="right") # if (len(columns) > 1) and cols > 1: # _p.opts(frame_width=width) if extension == "bokeh": _p.opts(frame_width=width) p[col] = _p except KeyError as e: print(f"Sorry - missing key: {col}") logging.debug(e) w = width / 180 * cols h = 5 * len(p) / cols if len(p) >= 1: if not individual: if simple: out = hv.Layout(list(p.values())).cols(cols) else: out = hv.NdLayout(p, sort=False).cols(cols) if extension == "matplotlib": out.opts(fig_inches=(w, h)) else: if extension == "matplotlib": out = [o.opts(fig_inches=(w, h)) for o in p.values()] else: out = [p.values()] return out
if __name__ == "__main__": pass