# -*- coding: utf-8 -*-
"""easyplot module for cellpy. It provides easy plotting of any cellpy-readable data using matplotlib.
Author: Amund M. Raniseth
Date: 01.07.2021
"""
import logging
import os
import warnings
from pathlib import Path
from re import S
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib import lines
from matplotlib.artist import kwdoc
from matplotlib.lines import Line2D
from matplotlib.scale import LogScale
from matplotlib.ticker import FuncFormatter
import cellpy
from cellpy import log
from cellpy.parameters.internal_settings import (
get_headers_journal,
keys_journal_session,
)
from cellpy.utils.batch_tools.batch_journals import LabJournal
[docs]
hdr_journal = get_headers_journal()
# Dictionary of all possible user input arguments(as keys) with example values of correct type
# Value is a tuple (immutable) of type and default value.
[docs]
USER_PARAMS = {
"cyclelife_plot": (bool, True),
"cyclelife_separate_data": (
bool,
False,
), # will plot each cyclelife datafile in separate plots
"cyclelife_percentage": (bool, False),
"cyclelife_coulombic_efficiency": (bool, False),
"cyclelife_coulombic_efficiency_ylabel": (str, "Coulombic efficiency [%]"),
"cyclelife_charge_c_rate": (bool, False),
"cyclelife_discharge_c_rate": (bool, False),
"cyclelife_c_rate_ylabel": (str, "Effective C-rate"),
"cyclelife_ir": (bool, False), # Allows user to plot IR data aswell
"cyclelife_xlabel": (str, "Cycles"),
"cyclelife_ylabel": (str, r"Capacity $\left[\frac{mAh}{g}\right]$"),
"cyclelife_ylabel_percent": (str, "Capacity retention [%]"),
"cyclelife_legend_outside": (
bool,
False,
), # if True, the legend is placed outside the plot
"cyclelife_degradation_slope": (
bool,
False,
), # Adds simple degradation slope regression to plot
"capacity_determination_from_ratecap": (
bool,
False,
), # If True, uses the ratecap and capacity to determine the exp capacity
"galvanostatic_plot": (bool, True),
"galvanostatic_potlim": (tuple, None), # min and max limit on potential-axis
"galvanostatic_caplim": (tuple, None),
"galvanostatic_xlabel": (str, r"Capacity $\left[\frac{mAh}{g}\right]$"),
"galvanostatic_ylabel": (str, "Cell potential [V]"),
"galvanostatic_normalize_capacity": (
bool,
False,
), # Normalizes all cycles' capacity to 1.
"dqdv_plot": (bool, False),
"dqdv_potlim": (tuple, None), # min and max limit on potential-axis
"dqdv_dqlim": (tuple, None),
"dqdv_xlabel": (
str,
r"dQ/dV $\left[\frac{mAh}{gV}\right]$",
), # TODO what unit? jees
"dqdv_ylabel": (str, "Cell potential [V]"),
"specific_cycles": (list, None),
"exclude_cycles": (list, None),
"all_in_one": (
bool,
False,
), # Decides if everything should be plotted in the same plot in GC and dQdV plot
"only_dischg": (bool, False), # Only show discharge curves
"only_chg": (bool, False), # Only show charge curves
"outpath": (str, "./"),
"outtype": (str, ".png"), # What file format to save in
"outname": (str, None), # Overrides the automatic filename generation
"figsize": (tuple, (6, 4)), # 6 inches wide, 4 inches tall
"figres": (int, 100), # Dots per Inch
"figtitle": (str, "Title"), # None = original filepath
"save_figures": (bool, True),
"save_journal": (bool, False), # Save journal
}
[docs]
def help():
"""Method of the EasyPlot class which prints some helptext in addition to all supported params."""
## Prints out help page of this module
help_str = (
"The easyplot extension to cellpy aims to easily plot data in a pretty manner.\n"
"In order to use this function, you must import cellpy, and easyplot from cellpy.utils.\n"
"\n"
"Usage:\n"
"Create list of datafiles you want to plot on the following format:\n"
"\n"
"files = [\n"
"\t'./folder/filename.ext',\n"
"\t'./folder/filename2.ext',\n"
"\t]\n"
"\n"
"And then call the easyplot.plot function with the files list as the first parameter, and any optional keyword arguments.\n"
"Here is an example of the use of all keyword arguments:\n"
)
for kw in USER_PARAMS:
if type(USER_PARAMS[kw][1]) == str:
insert = "'" + USER_PARAMS[kw][1] + "'"
else:
insert = str(USER_PARAMS[kw][1])
help_str += "\t" + kw + " = " + insert + ",\n"
print(help_str)
[docs]
class EasyPlot:
"""Main easyplot class.
Takes all the inputs from the user in its kwargs upon object initialization.
Gathers data, handles and plots it when object.plot() is called.
Help: type easyplot.help()
"""
def __init__(self, files=None, nicknames=None, journal=None, **kwargs):
"""Initialization function of the EasyPlot class.
Input parameters:
filenames (list of strings).
nicknames (list of strings), must match length of filenames.
journal (str or pathlib.Path object): journal file name (should not be used if files is given).
any kwargs: use easyplot.help() to print all kwargs to terminal.
Returns:
easyplot object
Most basic usage:
ezpltobj = easyplot.EasyPlot(["name1", "name2"], None)"""
# Make all user input variables of self
self.files = files
self.nicknames = nicknames
self.kwargs = kwargs
# More needed variables
self.figs = []
self.file_data = []
self.use_arbin_sql = False
if journal is not None:
self.journal_file = Path(journal)
else:
self.journal_file = None
self.journal = None
# Dictionary of all possible user input arguments(as keys) with example values of correct type
# Value is a tuple (immutable) of type and default value.
self.user_params = USER_PARAMS
# Create 'empty' attributes for later use
self.outpath = None
self.masses = None
self.labels = None
self.nom_caps = None
self.colors = None
# List of available colors
# Fill in the rest of the variables from self.user_params if the user didn't specify
self.fill_input()
# Verify that the user input is sufficient
self.verify_input()
self._generate_list_of_available_colors()
def _generate_list_of_available_colors(self):
if 19 >= len(self.files) > 10:
self.colors = [
"#e6194b",
"#3cb44b",
"#ffe119",
"#4363d8",
"#f58231",
"#911eb4",
"#46f0f0",
"#f032e6",
"#bcf60c",
"#fabebe",
"#008080",
"#e6beff",
"#9a6324",
"#fffac8",
"#800000",
"#aaffc3",
"#808000",
"#ffd8b1",
"#000075",
"#808080",
"#000000",
]
warnings.warn(
"You inserted more than 10 datafiles! In a desperate attempt to keep "
"the plots tidy, another colorpalette with 19 distinct colors were chosen."
)
elif len(self.files) > 19:
warnings.warn(
"You inserted more than 19 datafiles! We do not have that "
"many colors in the palette, this some colors are beeing recycled. "
"Keep track of the filenames and legends and make sure this doesn't confuse you."
)
else:
self.colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
] * 5
[docs]
def plot(self):
"""This is the method the user calls on his/hers easyplot object in order to gather the data and plot it.
Usage: object.plot()"""
# Load all cellpy files
logging.debug("starting plotting")
for file in self.files:
if isinstance(file, (list, tuple)):
logging.debug("linked files provided - need to merge")
linked_files = True
else:
linked_files = False
# If using arbin sql
if self.use_arbin_sql:
cpobj = cellpy.get(
filename=file, instrument="arbin_sql"
) # Initiate cellpy object
else: # Not Arbin SQL? Then its probably a local file
# Check that file(s) exist
if linked_files:
file_name = "_".join(file)
for _f in file:
if not os.path.isfile(_f):
logging.error("File not found: " + str(_f))
raise FileNotFoundError
else:
file_name = file
if not os.path.isfile(file):
logging.error("File not found: " + str(file))
print(os.getcwd())
raise FileNotFoundError
cpobj = cellpy.get(filename=file) # Load regular file
# Check that we get data
if cpobj is None:
warnings.warn(
f"File reader returned no data for filename {file}. Please make sure that the file exists or "
f"that the data exists in an eventual database."
)
# Get ID of all cycles
cyc_nums = cpobj.get_cycle_numbers()
# Only get the cycles which both exist in data, and that the user want
if self.kwargs["specific_cycles"] is not None:
cyc_not_available = (
set(cyc_nums) ^ set(self.kwargs["specific_cycles"])
) & set(self.kwargs["specific_cycles"])
if len(cyc_not_available) > 0:
warn_str = (
f"You want to plot cycles which are not available in the data! Datafile(s): "
f"{file}"
f", Cycle(s): {str(cyc_not_available)}"
)
warnings.warn(warn_str)
cyc_nums = list(
set(cyc_nums).intersection(self.kwargs["specific_cycles"])
)
if self.kwargs["exclude_cycles"] is not None:
cyc_nums = list(set(cyc_nums) - set(self.kwargs["exclude_cycles"]))
color = self.give_color() # Get a color for the data
self.file_data.append((cpobj, cyc_nums, color, file_name))
# Check kwargs/input parameters to see what plots to make
if self.kwargs["cyclelife_plot"]:
self.plot_cyclelife()
if self.kwargs["galvanostatic_plot"] and not self.kwargs["dqdv_plot"]:
self.plot_gc()
if self.kwargs["dqdv_plot"] and not self.kwargs["galvanostatic_plot"]:
self.plot_dQdV()
if self.kwargs["galvanostatic_plot"] and self.kwargs["dqdv_plot"]:
self.plot_gc_and_dQdV()
if self.kwargs["capacity_determination_from_ratecap"]:
self.plot_cap_from_rc()
self._wrap_up()
def _wrap_up(self):
# saving journal file
if self.kwargs["save_journal"]:
if self.journal is not None:
if self.outpath is not None:
journal_file_path = Path(self.outpath) / self.journal_file.name
else:
journal_file_path = self.journal_file.name
# if we want to enforce that the file will be a xlsx file:
# journal_file_path = journal_file_path.with_suffix(".xlsx")
journal_file_path = journal_file_path.with_suffix(".json")
self.journal.to_file(
file_name=journal_file_path, paginate=False, to_project_folder=False
)
xlsx_journal_file_path = journal_file_path.with_name(
f"{journal_file_path.stem}.xlsx"
)
self.journal.to_file(
file_name=xlsx_journal_file_path,
paginate=False,
to_project_folder=False,
)
def _read_journal_file(self):
logging.debug(f"reading journal file {self.journal_file}")
journal = LabJournal(db_reader=None)
journal.from_file(self.journal_file, paginate=False)
self.journal = journal
def _populate_from_journal(self):
logging.debug(f"populating from journal")
# populating from only a subset of the available journal columns
# - can be increased later
try:
self.files = self.journal.pages[hdr_journal["raw_file_names"]].to_list()
except AttributeError:
logging.debug("No raw files found in your journal")
try:
self.masses = self.journal.pages[hdr_journal["mass"]].to_list()
except AttributeError:
logging.debug("No masses found in your journal")
try:
self.labels = self.journal.pages[hdr_journal["label"]].to_list()
except AttributeError:
logging.debug("No labels found in your journal")
try:
self.nom_cap = self.journal.pages[hdr_journal["nom_cap"]].to_list()
except AttributeError:
logging.debug("No nominal capacity found in your journal")
try:
self.cellpy_files = self.journal.pages[
hdr_journal["cellpy_file_name"]
].to_list()
except AttributeError:
logging.debug("No cellpy files found in your journal")
[docs]
def set_arbin_sql_credentials(
self,
server="localhost",
uid="sa",
pwd="Changeme123",
driver="ODBC Driver 17 for SQL Server",
):
"""Sets cellpy.prms.Instruments.Arbin details to fit what is inserted.
Parameters: Server = 'IP of server', uid = 'username', pwd = 'password', driver = 'ODBC Driver 17 for SQL Server'
"""
cellpy.prms.Instruments.Arbin["SQL_server"] = server
cellpy.prms.Instruments.Arbin["SQL_UID"] = uid
cellpy.prms.Instruments.Arbin["SQL_PWD"] = pwd
cellpy.prms.Instruments.Arbin["SQL_Driver"] = driver
self.use_arbin_sql = True
[docs]
def give_color(self):
"""Picks the first color from the color list and gives it away"""
color = self.colors[0]
self.colors = self.colors[1:]
return color
[docs]
def give_fig(self):
"""Gives figure to whoever asks and appends it to figure list"""
fig, ax = plt.subplots(figsize=(6, 4))
self.figs.append((fig, ax))
return fig, ax
[docs]
def handle_outpath(self):
"""Makes sure that self.outpath exists, or creates it."""
out_path = self.kwargs["outpath"]
# should make this a pathlib.Path object - but not sure if str is assumed later on in the code
if os.path.isdir(out_path):
logging.debug(f"out path set to {out_path}")
return out_path
elif not os.path.isdir(out_path):
logging.debug(f"outpath does not exits - creating")
try:
os.makedirs(out_path)
logging.debug(f"out path set to {out_path}")
return out_path
except OSError as e:
logging.error(
f"Cannot create output directory {out_path}. Please make sure you "
f"have write permission. Error message: {e}"
)
[docs]
def plot_cyclelife(self):
"""Takes all the parameters inserted in the object creation and plots cyclelife"""
# Spawn fig and axis for plotting
if not self.kwargs["cyclelife_separate_data"]:
fig, ax = self.give_fig()
if self.kwargs["cyclelife_coulombic_efficiency"]:
# Spawn twinx axis and set label
ax_ce = ax.twinx()
ax_ce.set(ylabel=self.kwargs["cyclelife_coulombic_efficiency_ylabel"])
if (
self.kwargs["cyclelife_charge_c_rate"]
or self.kwargs["cyclelife_discharge_c_rate"]
):
ax_c_rate = ax.twinx()
def format_label(x, pos):
# The commented out code here makes the fractioned C-rate like C/50 and so on.
"""
if x >= 1:
s = '%.2gC' % x
elif x == 0:
s = r'C/$\infty$'
else:
newfloat = 1/x
s = 'C/%.2g' % newfloat
"""
# The following just has decimal place C-rate.
s = "%.3gC" % x
return s
ax_c_rate.yaxis.set_major_formatter(FuncFormatter(format_label))
ax_c_rate.set(ylabel="Effective C-rate")
if self.kwargs["cyclelife_ir"]:
ax_ir = ax.twinx()
outpath = self.outpath
for cpobj, cyc_nums, color, filename in self.file_data:
if self.kwargs["cyclelife_separate_data"]:
fig, ax = self.give_fig()
if self.kwargs["cyclelife_coulombic_efficiency"]:
# Spawn twinx axis and set label
ax_ce = ax.twinx()
ax_ce.set(
ylabel=self.kwargs["cyclelife_coulombic_efficiency_ylabel"]
)
if (
self.kwargs["cyclelife_charge_c_rate"]
or self.kwargs["cyclelife_discharge_c_rate"]
):
ax_c_rate = ax.twinx()
def format_label(x, pos):
# The following just has decimal place C-rate.
s = "%.3gC" % x
return s
ax_c_rate.yaxis.set_major_formatter(FuncFormatter(format_label))
ax_c_rate.set(ylabel="Effective C-rate")
if self.kwargs["cyclelife_ir"]:
ax_ir = ax.twinx()
# Get Pandas DataFrame of pot vs cap from cellpy object
df = cpobj.get_cap(
method="forth-and-forth",
label_cycle_number=True,
categorical_column=True,
)
outpath += os.path.basename(filename).split(".")[0] + "_"
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
chgs = [[], []] # List with cycle num and capacity
dchgs = [[], []]
# Accumulate cycles
for cyc in keys: # Loop over all cycles
if cyc in cyc_nums: # Check if it is in list of wanted cycles
cyc_df = cycgrouped.get_group(
cyc
) # Get the group of datapoints from specific cycle
cyc_redox_grouped = cyc_df.groupby(
"direction"
) # Group by direction (meaning if it is charging or discharging)
dchg_df = cyc_redox_grouped.get_group(
-1
) # Data for the discharge curve
dchgs[0].append(cyc) # Append to dchg list
dchgs[1].append(dchg_df["capacity"].iat[-2])
chg_df = cyc_redox_grouped.get_group(1) # Data for the charge curve
chgs[0].append(cyc) # Append to chg list
chgs[1].append(chg_df["capacity"].iat[-2])
if self.kwargs[
"cyclelife_percentage"
]: # Normalize all datapoints on the first one
norm_fact = (
dchgs[1][0] / 100
) # /100 is to get range from 0-100(%) in stead of 0-1
for i in range(len(chgs[1])):
chgs[1][i] /= norm_fact
for i in range(len(dchgs[1])):
dchgs[1][i] /= norm_fact
# Make label from filename or nickname
if self.nicknames:
label = self.nicknames[self.files.index(filename)]
else:
label = str(os.path.basename(filename))
# print("Discharge capacities:")
# print(dchgs[1])
# Actully place it in plot
if not self.kwargs["only_dischg"] and not self.kwargs["only_chg"]:
ax.scatter(chgs[0], chgs[1], c=color, alpha=0.2)
ax.scatter(dchgs[0], dchgs[1], c=color, label=label)
elif self.kwargs["only_dischg"]:
ax.scatter(dchgs[0], dchgs[1], c=color, label=label)
elif self.kwargs["only_chg"]:
ax.scatter(chgs[0], chgs[1], c=color, alpha=0.2)
if self.kwargs["cyclelife_coulombic_efficiency"]:
# Get CE for cyc_nums
coulombic_efficiency = cpobj.data.summary[
"coulombic_efficiency_u_percentage"
]
cycs = []
CEs = []
for cyc in keys:
if cyc in cyc_nums:
cycs.append(cyc)
CEs.append(coulombic_efficiency[cyc])
# Place it in the plot
ax_ce.scatter(cycs, CEs, c=color, marker="+")
# print(filename + " Dchg 1-3: " + str(dchgs[1][0:3]) + ", CE 1-3: " + str(coulombic_efficiency[0:3]))
if (
self.kwargs["cyclelife_charge_c_rate"]
or self.kwargs["cyclelife_discharge_c_rate"]
):
# charge_c_rate = cpobj.data.summary["charge_c_rate"] #This gives incorrect c-rates.
stepstable = cpobj.data.steps
chg_c_rates, dchg_c_rates = get_effective_C_rates(stepstable)
selected_chg_c_rates = []
selected_dchg_c_rates = []
selected_cycs = []
for cyc in keys:
if cyc in cyc_nums:
selected_chg_c_rates.append(chg_c_rates[cyc - 1])
selected_dchg_c_rates.append(dchg_c_rates[cyc - 1])
selected_cycs.append(cyc)
if (
self.kwargs["cyclelife_charge_c_rate"]
and not self.kwargs["cyclelife_discharge_c_rate"]
):
ax_c_rate.scatter(
selected_cycs, selected_chg_c_rates, c=color, marker="_"
)
elif (
not self.kwargs["cyclelife_charge_c_rate"]
and self.kwargs["cyclelife_discharge_c_rate"]
):
ax_c_rate.scatter(
selected_cycs, selected_dchg_c_rates, c=color, marker="_"
)
elif (
self.kwargs["cyclelife_charge_c_rate"]
and self.kwargs["cyclelife_discharge_c_rate"]
):
ax_c_rate.scatter(
selected_cycs, selected_chg_c_rates, c=color, marker="_"
)
ax_c_rate.scatter(
selected_cycs,
selected_dchg_c_rates,
c=color,
alpha=0.2,
marker="_",
)
if self.kwargs["cyclelife_degradation_slope"]:
from scipy.stats import linregress
slope, intercept, r, p, se = linregress(dchgs[0], dchgs[1])
x = np.linspace(0, ax.get_xlim()[1] * 0.9, 10)
degradation_unit = (
r" $\frac{mAh}{g\cdot cycle}$"
if not self.kwargs["cyclelife_percentage"]
else r" $\frac{\%}{cycle}$"
)
intercept_unit = (
r" $\frac{mAh}{g}$"
if not self.kwargs["cyclelife_percentage"]
else r"%"
)
ax.plot(
x,
x * slope + intercept,
c=color,
label="Degradation: %g" % slope
+ degradation_unit
+ "\nIntercept: %g" % intercept
+ intercept_unit
+ ", r=%g" % r,
)
"""if self.kwargs["cyclelife_ir"]:
chg_ir = []
dchg_ir = []
steptable = cpobj.steps
print(steptable)
newdf = steptable[["ir", "cycle", "type"]]
for i,elem in enumerate(newdf.iterrows()):
if elem[1]["type"] == "charge":
chg_ir.append(elem[1]["ir"])
elif elem[1]["type"] == "discharge":
dchg_ir.append(elem[1]["ir"])
print(chg_ir)
for cyc in keys:
if cyc in cyc_nums:
ax_ir.scatter(cyc, chg_ir[cyc], c = color, marker = "*")
"""
if self.kwargs["cyclelife_separate_data"]:
# Set all plot settings from Plot object
self.fix_cyclelife(fig, ax)
# Save fig
savepath = outpath.strip("_") + "_Cyclelife"
self.save_fig(fig, savepath)
if not self.kwargs["cyclelife_separate_data"]:
# Set all plot settings from Plot object
self.fix_cyclelife(fig, ax)
# Save fig
savepath = outpath.strip("_") + "_Cyclelife"
self.save_fig(fig, savepath)
[docs]
def plot_gc(self):
"""Takes all the parameters inserted in the object creation and plots Voltage-Capacity curves"""
if self.kwargs["all_in_one"]: # Everything goes in the same figure.
fig, ax = self.give_fig()
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
] * 5
savepath = self.outpath
colorbar_incrementor = -1
for cpobj, cyc_nums, color, filename in self.file_data:
# Get Pandas DataFrame of pot vs cap from cellpy object
df = cpobj.get_cap(
method="forth-and-forth",
label_cycle_number=True,
categorical_column=True,
)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Make label from filename or nickname
if self.nicknames:
label = str(self.nicknames[self.files.index(filename)])
else:
label = str(os.path.basename(filename))
# Fix colorbar or cycle colors
if self.kwargs["specific_cycles"] == None: # Plot all cycles
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"name", [color, "black"], N=256, gamma=1.0
)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
cbaxes = fig.add_axes(
[1.05 + colorbar_incrementor / 8, 0.1, 0.03, 0.8]
)
colorbar_incrementor += 1
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
cax=cbaxes,
label="Cycle number for "
+ os.path.basename(filename).split(".")[0],
)
# fig.colorbar.ax.yaxis.get_major_locator().set_params(integer=True) #TODO fix such that we dont have decimals on the cycle colorbar!!
# Plot cycles
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
if (
not self.kwargs["only_dischg"]
and not self.kwargs["only_chg"]
):
pass
elif self.kwargs["only_dischg"]:
dchg = cyc_df.groupby("direction")
cyc_df = dchg.get_group(-1)
elif self.kwargs["only_chg"]:
chg = cyc_df.groupby("direction")
cyc_df = chg.get_group(1)
# TODO: The way this is set up, when plotting both discharge and charge, the whole cycle
# is normalized on the maximum capacity, meaning the charge can be normalized on
# the discharge or the other way around.
if self.kwargs["galvanostatic_normalize_capacity"]:
# Then we normalize capacity column on the max value (since this should be max cap)
maxcap = cyc_df["capacity"].max()
cyc_df["capacity"] = cyc_df["capacity"].div(maxcap)
ax.set_xlabel("Normalized Capacity")
ax.plot(
cyc_df["capacity"],
cyc_df["voltage"],
label=label + ", Cyc " + str(cyc),
c=cyccolor,
)
savepath += os.path.basename(filename).split(".")[0]
fig.suptitle("Galvanostatic cyclingdata")
self.fix_gc(fig, ax)
# Save fig
savepath += "_GC-plot"
self.save_fig(fig, savepath)
else: # Then each data goes in its own figure
for cpobj, cyc_nums, color, filename in self.file_data:
fig, ax = self.give_fig()
# Get Pandas DataFrame of pot vs cap from cellpy object
df = cpobj.get_cap(
method="forth-and-forth",
label_cycle_number=True,
categorical_column=True,
)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Fix colorbar or cycle colors
if self.kwargs["specific_cycles"] == None: # Plot all cycles
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"name", [color, "black"], N=256, gamma=1.0
)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap), label="Cycle"
)
# fig.colorbar.ax.yaxis.get_major_locator().set_params(integer=True) #TODO fix such that we dont have decimals on the cycle colorbar!!
# Make label from filename or nickname
if self.nicknames:
label = str(self.nicknames[self.files.index(filename)])
else:
label = str(os.path.basename(filename))
# Plot cycles
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
]
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
# TODO: This if elif block is pretty much the same as the one above (for all in one plot), can it be reused in stead of written twice?
if (
not self.kwargs["only_dischg"]
and not self.kwargs["only_chg"]
):
pass
elif self.kwargs["only_dischg"]:
dchg = cyc_df.groupby("direction")
cyc_df = dchg.get_group(-1)
elif self.kwargs["only_chg"]:
chg = cyc_df.groupby("direction")
cyc_df = chg.get_group(1)
# TODO: The way this is set up, when plotting both discharge and charge, the whole cycle is normalized on the maximum capacity, meaning the charge can be normalized on the discharge or the other way around.
if self.kwargs["galvanostatic_normalize_capacity"]:
# Then we normalize capacity column on the max value (since this should be max cap)
maxcap = cyc_df["capacity"].max()
cyc_df["capacity"] = cyc_df["capacity"].div(maxcap)
ax.set_xlabel("Normalized Capacity")
ax.plot(
cyc_df["capacity"],
cyc_df["voltage"],
label=label.split(".")[0] + ", Cyc " + str(cyc),
c=cyccolor,
)
# Set all plot settings from Plot object
fig.suptitle(label)
self.fix_gc(fig, ax)
# Save fig
savepath = (
self.outpath + os.path.basename(filename).split(".")[0] + "_GC-plot"
)
self.save_fig(fig, savepath)
[docs]
def plot_dQdV(self):
"""Takes all the parameters inserted in the object creation and plots dQdV"""
from cellpy.utils import ica
if self.kwargs["all_in_one"]: # Everything goes in the same figure.
fig, ax = self.give_fig()
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
] * 5
savepath = self.outpath
colorbar_incrementor = -1
for cpobj, cyc_nums, color, filename in self.file_data:
# Get Pandas DataFrame of dQdV
if self.kwargs["only_dischg"]:
_, df = ica.dqdv(cpobj, split=True)
elif self.kwargs["only_chg"]:
df, _ = ica.dqdv(cpobj, split=True)
else:
df = ica.dqdv(cpobj)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Fix colorbar or cycle colors
if self.kwargs["specific_cycles"] == None: # Plot all cycles
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"name", [color, "black"], N=256, gamma=1.0
)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
cbaxes = fig.add_axes(
[1.05 + colorbar_incrementor / 8, 0.1, 0.03, 0.8]
)
colorbar_incrementor += 1
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
cax=cbaxes,
label="Cycle number for "
+ os.path.basename(filename).split(".")[0],
)
# fig.colorbar.ax.yaxis.get_major_locator().set_params(integer=True) #TODO fix such that we dont have decimals on the cycle colorbar!!
# Plot cycles
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
ax.plot(
cyc_df["voltage"],
cyc_df["dq"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
savepath += os.path.basename(filename).split(".")[0]
fig.suptitle("dQdV")
self.fix_dqdv(fig, ax)
# Save fig
savepath += "_dQdV-plot"
self.save_fig(fig, savepath)
else:
for cpobj, cyc_nums, color, filename in self.file_data:
fig, ax = self.give_fig()
# Get Pandas DataFrame of dQdV
if self.kwargs["only_dischg"]:
_, df = ica.dqdv(cpobj, split=True)
elif self.kwargs["only_chg"]:
df, _ = ica.dqdv(cpobj, split=True)
else:
df = ica.dqdv(cpobj)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Create the plot obj
fig, ax = plt.subplots(figsize=(6, 4))
# Fix colorbar or cycle colors
if self.kwargs["specific_cycles"] == None: # Plot all cycles
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"name", [color, "black"], N=256, gamma=1.0
)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap), label="Cycle"
)
# fig.colorbar.ax.yaxis.get_major_locator().set_params(integer=True) #TODO fix such that we dont have decimals on the cycle colorbar!!
# Plot cycles
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
]
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
ax.plot(
cyc_df["voltage"],
cyc_df["dq"],
label="Cycle " + str(cyc),
c=cyccolor,
)
# Set all plot settings from Plot object
fig.suptitle(os.path.basename(filename))
self.fix_dqdv(fig, ax)
# Save fig
savepath = (
self.outpath
+ os.path.basename(filename).split(".")[0]
+ "_dQdV-plot"
)
self.save_fig(fig, savepath)
[docs]
def plot_gc_and_dQdV(self):
"""Takes all the parameters inserted in the object creation and plots Voltage-Curves and dQdV data together"""
from cellpy.utils import ica
if self.kwargs["all_in_one"]: # Everything goes in the same figure.
fig, ax = self.give_fig()
fig.delaxes(ax)
ax1, ax2 = fig.subplots(1, 2, sharey=True)
fig.set_size_inches(8, 4)
fig.subplots_adjust(wspace=0)
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
] * 5
savepath = self.outpath
colorbar_incrementor = -1
for cpobj, cyc_nums, color, filename in self.file_data:
# Get Pandas DataFrame of pot vs cap from cellpy object
df = cpobj.get_cap(
method="forth-and-forth",
label_cycle_number=True,
categorical_column=True,
)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Fix colorbar or cycle colors
if self.kwargs["specific_cycles"] == None: # Plot all cycles
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"name", [color, "black"], N=256, gamma=1.0
)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
cbaxes = fig.add_axes(
[1.05 + colorbar_incrementor / 8, 0.1, 0.03, 0.8]
)
colorbar_incrementor += 1
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap),
cax=cbaxes,
label="Cycle number for "
+ os.path.basename(filename).split(".")[0],
pad=0.2,
)
# Plot GC in leftmost plot (ax)
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
if (
not self.kwargs["only_dischg"]
and not self.kwargs["only_chg"]
):
ax1.plot(
cyc_df["capacity"],
cyc_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
elif self.kwargs["only_dischg"]:
dchg = cyc_df.groupby("direction")
dchg_df = dchg.get_group(-1)
ax1.plot(
dchg_df["capacity"],
dchg_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
elif self.kwargs["only_chg"]:
chg = cyc_df.groupby("direction")
chg_df = chg.get_group(1)
ax1.plot(
chg_df["capacity"],
chg_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
# Get Pandas DataFrame for dQdV
if self.kwargs["only_dischg"]:
_, df = ica.dqdv(cpobj, split=True)
elif self.kwargs["only_chg"]:
df, _ = ica.dqdv(cpobj, split=True)
else:
df = ica.dqdv(cpobj)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Plot cycles
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
ax2.plot(
cyc_df["dq"],
cyc_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
savepath += os.path.basename(filename).split(".")[0]
# Set all plot settings from Plot object
fig.suptitle("GC and dQdV")
self.fix_gc_and_dqdv(fig, [ax1, ax2])
# Save fig
savepath = savepath + "_GC-dQdV-plot"
self.save_fig(fig, savepath)
else: # Then all files are placed in separate plots
for cpobj, cyc_nums, color, filename in self.file_data:
fig, ax = self.give_fig()
fig.delaxes(ax)
ax1, ax2 = fig.subplots(1, 2, sharey=True)
fig.set_size_inches(8, 4)
fig.subplots_adjust(wspace=0)
colors = [
"tab:blue",
"tab:orange",
"tab:green",
"tab:red",
"tab:purple",
"tab:brown",
"tab:pink",
"tab:gray",
"tab:olive",
"tab:cyan",
] * 5
# Get Pandas DataFrame of pot vs cap from cellpy object
df = cpobj.get_cap(
method="forth-and-forth",
label_cycle_number=True,
categorical_column=True,
)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Fix colorbar or cycle colors
if self.kwargs["specific_cycles"] == None: # Plot all cycles
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list(
"name", [color, "black"], N=256, gamma=1.0
)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
fig.colorbar(
mpl.cm.ScalarMappable(norm=norm, cmap=cmap), label="Cycle"
)
# Plot GC in leftmost plot (ax)
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
if (
not self.kwargs["only_dischg"]
and not self.kwargs["only_chg"]
):
ax1.plot(
cyc_df["capacity"],
cyc_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
elif self.kwargs["only_dischg"]:
dchg = cyc_df.groupby("direction")
dchg_df = dchg.get_group(-1)
ax1.plot(
dchg_df["capacity"],
dchg_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
elif self.kwargs["only_chg"]:
chg = cyc_df.groupby("direction")
chg_df = chg.get_group(1)
ax1.plot(
chg_df["capacity"],
chg_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
# Get Pandas DataFrame for dQdV
if self.kwargs["only_dischg"]:
_, df = ica.dqdv(cpobj, split=True)
elif self.kwargs["only_chg"]:
df, _ = ica.dqdv(cpobj, split=True)
else:
df = ica.dqdv(cpobj)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Plot cycles
for cyc in keys:
if cyc in cyc_nums:
if self.kwargs["specific_cycles"]:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc / keys[-1])
cyc_df = cycgrouped.get_group(cyc)
ax2.plot(
cyc_df["dq"],
cyc_df["voltage"],
label=os.path.basename(filename).split(".")[0]
+ ", Cyc "
+ str(cyc),
c=cyccolor,
)
# Set all plot settings from Plot object
fig.suptitle(os.path.basename(filename))
self.fix_gc_and_dqdv(fig, [ax1, ax2])
# Save fig
savepath = (
self.outpath
+ os.path.basename(filename).split(".")[0]
+ "_GC-dQdV-plot"
)
self.save_fig(fig, savepath)
"""# Fix colorbar or cycle colors
if not specific_cycles: # If this is none, then plot all!
# Set up colormap and add colorbar
cmap = mpl.colors.LinearSegmentedColormap.from_list("name", [color, "black"], N=256, gamma=1.0)
norm = mpl.colors.Normalize(vmin=cyc_nums[0], vmax=cyc_nums[-1])
fig.colorbar(mpl.cm.ScalarMappable(norm=norm, cmap=cmap),label='Cycle')
## Plot GC on the left subplot (ax[0]) ##
# Get Pandas DataFrame of pot vs cap from cellpy object
df = cpobj.get_cap(method="forth-and-forth", label_cycle_number=True, categorical_column=True)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Plot cycles
colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple', 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan' ]
for cyc in keys:
if cyc in cyc_nums:
if specific_cycles:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc/keys[-1])
cyc_df = cycgrouped.get_group(cyc)
axs[0].plot(cyc_df["capacity"], cyc_df["voltage"], label="Cycle " + str(cyc), c = cyccolor)
## Plot dQdV on the right subplot (ax[1]) ##
from cellpy.utils import ica
# Get Pandas DataFrame of pot vs cap from cellpy object
df = ica.dqdv(cpobj)
# Group by cycle and make list of cycle numbers
cycgrouped = df.groupby("cycle")
keys = []
for key, item in cycgrouped:
keys.append(key)
# Plot cycles
colors = ['tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple', 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan' ]
for cyc in keys:
if cyc in cyc_nums:
if specific_cycles:
cyccolor = colors[0]
colors = colors[1:]
else:
cyccolor = cmap(cyc/keys[-1])
cyc_df = cycgrouped.get_group(cyc)
axs[1].plot(cyc_df["dq"], cyc_df["voltage"], label=str(cyc), c = cyccolor)
# Set all plot settings from Plot object
fig.suptitle(os.path.basename(file))
self.fix_gc_and_dqdv(fig, axs)
# Save fig
savepath = self.outpath + os.path.basename(file).split(".")[0] + "_GC-dQdV-plot"
print("Saving to: " + savepath)
fig.savefig(savepath, bbox_inches='tight')"""
[docs]
def plot_cap_from_rc(self):
"""Takes all the parameters inserted in the object creation and plots capacity VS inverse c-rate"""
# Spawn fig and axis for plotting
fig, ax = self.give_fig()
# Get labels and handles for legend generation and eventual savefile
handles, labels = ax.get_legend_handles_labels()
# handles.append(Line2D([0], [0], marker='o', color='black', alpha = 0.2, label = 'Charge capacity', linestyle=''))
# handles.append(Line2D([0], [0], marker='o', color='black', alpha = 0.2, label = 'Disharge capacity', linestyle=''))
# handles.append(Line2D([0], [0], marker='+', color='black', label = 'Cap avg per C-rate', linestyle=''))
outpath = self.outpath
for cpobj, cyc_nums, color, filename in self.file_data:
# Get Pandas DataFrame of pot vs cap from cellpy object
# df = cpobj.get_cap(method="forth-and-forth", label_cycle_number=True, categorical_column=True)
outpath += os.path.basename(filename).split(".")[0] + "_"
handles.append(
Line2D([0], [0], marker="o", color=color, label=filename, linestyle="")
)
stepstable = cpobj.data.steps
chglist, dchglist = get_effective_C_rates_and_caps(stepstable)
# Remove all cycles which are not in cyc_nums by looking at the 0th element (cyc num) of every sublist in chglist
new_chglist = [x for x in chglist if x[0] in cyc_nums]
new_dchglist = [x for x in dchglist if x[0] in cyc_nums]
linregress_xlist = []
linregress_ylist = []
for chg, dchg in zip(new_chglist, new_dchglist):
# print(dchg)
# ax.scatter(chg[1] , chg[2] , color = color, alpha = 0.2)
ax.scatter(1 / dchg[1], dchg[2], color=color, alpha=1)
linregress_xlist.append(1 / dchg[1])
linregress_ylist.append(dchg[2])
# print(linregress_ylist)
# Fitting curve to the exponential function
# Import curve fitting package from scipy
# from scipy.optimize import curve_fit
x_arr = np.array(linregress_xlist)
y_arr = np.array(linregress_ylist)
# Average the capacity for each c-rate
def _reduce_to_averages(xvals, yvals):
"""This function scans through the data and averages relevant points together."""
point_grouped = []
point_lst = []
dists = []
for i in range(1, len(xvals)):
prev_point = np.array((xvals[i - 1], yvals[i - 1]))
curr_point = np.array((xvals[i], yvals[i]))
dev = 0.3
if (
(prev_point * (1 - dev))[0]
< curr_point[0]
< (prev_point * (1 + dev))[0]
):
# If this point is within dev (percentage sort of) of last point, then its in the same c-rate
point_lst.append(curr_point)
else:
# New c-rate
point_grouped.append(point_lst)
point_lst = []
print(point_grouped)
x_arr = []
y_arr = []
for group in point_grouped:
stacked_arr = np.stack(group, axis=1)
averaged_arr = np.average(stacked_arr, axis=1)
x_arr.append(averaged_arr[0])
y_arr.append(averaged_arr[1])
print(x_arr)
print(y_arr)
return x_arr, y_arr
# x_arr, y_arr = _reduce_to_averages(x_arr, y_arr)
# ax.scatter(x_arr, y_arr, marker="+")
# def _exp_func(x,a,b,c):
# return -a* (b**x) + a + -a * (b**(x+c)) +a
# pars, cov = curve_fit(f=_exp_func, p0 = [50, 0.7, 0], xdata = x_arr, ydata=y_arr, bounds = ([0,0.1, -20],[1e9, 1, 20]))
# x_vals = np.linspace(min(x_arr), max(x_arr), 100) #x_arr[0], x_arr[-1], 100)
# ax.plot(x_vals, _exp_func(x_vals, *pars))
# ax.hlines(max(y_arr), ax.get_xlim()[0], ax.get_xlim()[1], colors = color, linestyle='--')
# Get the standard deviations of the parameters (square roots of the # diagonal of the covariance)
# std_dev = np.sqrt(np.diag(cov))
# Make a sweet legend to put on this
# handles.append(
# Line2D(
# [0], [0],
# marker="_", color=color,
# label = 'Calculated maximum capacity:' + '\n' +'{:.2e} $\pm$ {:.2e}'.format(pars[0], std_dev[0]) + r'$\left[\mu Ah\right]$', linestyle=''
# ))
ax.hlines(
max(y_arr),
ax.get_xlim()[0],
ax.get_xlim()[1],
colors=color,
linestyle="--",
)
handles.append(
Line2D(
[0],
[0],
marker="_",
color=color,
label="Highest capacity:"
+ "\n"
+ "{:.2e}".format(max(y_arr))
+ r"$\left[\mu Ah\right]$",
linestyle="",
)
)
self.fix_cap_from_rc(fig, ax, handles)
# Save fig
savepath = outpath + "CapDet"
self.save_fig(fig, savepath)
[docs]
def fix_cyclelife(self, fig, ax):
"""Makes the finishing touches to the cyclelife plot"""
# Applies kwargs settings and other plot settings
## Parameters which could be user defined later
"""
ax.set(
xticks = (np.arange(0, 150), step=20)),
yticks = (np.arange(3, 5, step=0.2)),
)
"""
# Get labels and handles for legend generation and eventual savefile
handles, labels = ax.get_legend_handles_labels()
if not self.kwargs["only_dischg"]:
handles.append(
Line2D(
[0],
[0],
marker="o",
color="black",
alpha=0.2,
label="Charge capacity",
linestyle="",
)
)
if self.kwargs["cyclelife_coulombic_efficiency"]:
handles.append(
Line2D(
[0],
[0],
marker="+",
color="black",
alpha=1,
label="Coulombic Efficiency",
linestyle="",
)
)
if (
self.kwargs["cyclelife_charge_c_rate"]
and not self.kwargs["cyclelife_discharge_c_rate"]
):
handles.append(
Line2D(
[0],
[0],
marker="_",
color="black",
alpha=1,
label="Effective charge C-rate",
linestyle="",
)
)
elif (
not self.kwargs["cyclelife_charge_c_rate"]
and self.kwargs["cyclelife_discharge_c_rate"]
):
handles.append(
Line2D(
[0],
[0],
marker="_",
color="black",
alpha=1,
label="Effective discharge C-rate",
linestyle="",
)
)
elif (
self.kwargs["cyclelife_charge_c_rate"]
and self.kwargs["cyclelife_discharge_c_rate"]
):
handles.append(
Line2D(
[0],
[0],
marker="_",
color="black",
alpha=1,
label="Effective charge C-rate",
linestyle="",
)
)
handles.append(
Line2D(
[0],
[0],
marker="_",
color="black",
alpha=0.2,
label="Effective discharge C-rate",
linestyle="",
)
)
# The params below should always be like this.
ax.tick_params(direction="in", top="true", right="true")
ax.xaxis.get_major_locator().set_params(integer=True)
# Apply all kwargs to plot
try:
# Cyclelife plot details
ax.set(xlabel=self.kwargs["cyclelife_xlabel"])
if self.kwargs["cyclelife_percentage"]:
ax.set(ylabel=self.kwargs["cyclelife_ylabel_percent"])
else:
ax.set(ylabel=self.kwargs["cyclelife_ylabel"])
# General plot details
fig.set_size_inches(self.kwargs["figsize"])
if type(self.kwargs["figtitle"]) == str:
fig.suptitle(self.kwargs["figtitle"])
else:
fig.suptitle("Capacity versus Cycle life")
except Exception as e:
logging.error(e)
# Take care of having the legend outside the plot
if self.kwargs["cyclelife_legend_outside"]:
if (
self.kwargs["cyclelife_coulombic_efficiency"]
or self.kwargs["cyclelife_charge_c_rate"]
or self.kwargs["cyclelife_discharge_c_rate"]
):
ax.legend(handles=handles, bbox_to_anchor=(1.18, 1), loc="upper left")
else:
ax.legend(handles=handles, bbox_to_anchor=(1.05, 1), loc="upper left")
figsize = self.kwargs["figsize"]
fig.set_size_inches((figsize[0] + 3, figsize[1]))
else:
ax.legend(handles=handles)
fig.tight_layout() # Needed to not clip ylabel on coulombic efficiency
[docs]
def fix_cap_from_rc(self, fig, ax, handles):
"""Makes the finishing touches to the capacity vs inverse C-rate plot"""
ax.tick_params(direction="in", top="true", right="true")
ax.set(
xlabel=r"Inverse C-rate $\left[ h \right]$",
ylabel=r"Capacity $\left[\mu Ah \right]$",
)
# General plot details
fig.set_size_inches(self.kwargs["figsize"])
if type(self.kwargs["figtitle"]) == str:
fig.suptitle(self.kwargs["figtitle"])
else:
fig.suptitle("Capacity determination from Rate Capability")
# Take care of having the legend outside the plot
if self.kwargs["cyclelife_legend_outside"]:
ax.legend(handles=handles, bbox_to_anchor=(1.05, 1), loc="upper left")
figsize = self.kwargs["figsize"]
fig.set_size_inches((figsize[0] + 3, figsize[1]))
else:
ax.legend(handles=handles)
fig.tight_layout() # Needed to not clip ylabel on coulombic efficiency
[docs]
def fix_gc(self, fig, ax):
"""Makes the finishing touches to the voltage-curves plot"""
# Applies kwargs settings and other plot settings
## Parameters which could be user defined later
"""
ax.set(
xticks = (np.arange(0, 150), step=20)),
yticks = (np.arange(3, 5, step=0.2)),
)
"""
# The params below should always be like this.
ax.tick_params(direction="in", top="true", right="true")
# Apply all kwargs to plot
try:
# Galvanostatic plot details
ax.set(xlabel=self.kwargs["galvanostatic_xlabel"])
ax.set(ylabel=self.kwargs["galvanostatic_ylabel"])
ax.set(ylim=self.kwargs["galvanostatic_potlim"])
ax.set(xlim=self.kwargs["galvanostatic_caplim"])
if self.kwargs["specific_cycles"] != None:
ax.legend()
# General plot details
fig.set_size_inches(self.kwargs["figsize"])
if type(self.kwargs["figtitle"]) == str:
fig.suptitle(self.kwargs["figtitle"])
except Exception as e:
logging.error(e)
[docs]
def fix_dqdv(self, fig, ax):
"""Makes the finishing touches to the dQdV plot"""
# Applies kwargs settings and other plot settings
## Parameters which could be user defined later
"""
ax.set(
xticks = (np.arange(0, 150), step=20)),
yticks = (np.arange(3, 5, step=0.2)),
)
"""
# The params below should always be like this.
ax.tick_params(direction="in", top="true", right="true")
# Apply all kwargs to plot
try:
# Cyclelife plot details
ax.set(xlabel=self.kwargs["dqdv_xlabel"])
ax.set(ylabel=self.kwargs["dqdv_ylabel"])
ax.set(ylim=self.kwargs["dqdv_dqlim"])
ax.set(xlim=self.kwargs["dqdv_potlim"])
if self.kwargs["specific_cycles"] != None:
ax.legend()
# General plot details
fig.set_size_inches(self.kwargs["figsize"])
if type(self.kwargs["figtitle"]) == str:
fig.suptitle(self.kwargs["figtitle"])
except Exception as e:
logging.error(e)
[docs]
def fix_gc_and_dqdv(self, fig, axs):
"""Makes the finishing touches to the dQdV / Voltage curves plot"""
for ax in axs:
# The params below should always be like this.
ax.tick_params(direction="in", top="true", right="true")
# Apply all kwargs to plot
try:
# dQdV plot details
axs[1].set(
xlabel=self.kwargs["dqdv_ylabel"]
) # switched x and y label since this dQdV plot is flipped to match the adjacent gc plot
axs[1].set(ylabel="") # Empty since we already have potential on gc axs
axs[1].set(ylim=self.kwargs["galvanostatic_potlim"])
axs[1].set(xlim=self.kwargs["dqdv_dqlim"])
# Galvanostatic plot details
axs[0].set(xlabel=self.kwargs["galvanostatic_xlabel"])
axs[0].set(ylabel=self.kwargs["galvanostatic_ylabel"])
axs[0].set(ylim=self.kwargs["galvanostatic_potlim"])
axs[0].set(xlim=self.kwargs["galvanostatic_caplim"])
if self.kwargs["specific_cycles"] != None:
axs[0].legend()
# General plot details
fig.set_size_inches(self.kwargs["figsize"])
if type(self.kwargs["figtitle"]) == str:
fig.suptitle(self.kwargs["figtitle"])
except Exception as e:
print(e)
logging.error(e)
[docs]
def save_fig(self, fig, savepath):
"""The point of this is to have savefig parameters the same across
all plots (for now just fig dpi and bbox inches)"""
if self.kwargs.get("save_figures", True):
if self.kwargs["outname"]:
savepath = (
self.kwargs["outpath"]
+ self.kwargs["outname"]
+ self.kwargs["outtype"]
)
else:
savepath += self.kwargs["outtype"]
print("Saving to: " + savepath)
fig.savefig(savepath, bbox_inches="tight", dpi=self.kwargs["figres"])
[docs]
def get_effective_C_rates(steptable):
newdf = steptable[["step_time_avr", "cycle", "type"]]
chg_c_rates = []
dchg_c_rates = []
for i, elem in enumerate(newdf.iterrows()):
if elem[1]["type"] == "charge":
chg_c_rates.append(1 / (elem[1]["step_time_avr"] / 3600))
elif elem[1]["type"] == "discharge":
dchg_c_rates.append(1 / (elem[1]["step_time_avr"] / 3600))
return chg_c_rates, dchg_c_rates
[docs]
def get_effective_C_rates_and_caps(steptable):
newdf = steptable[
["step_time_avr", "cycle", "type", "charge_avr", "discharge_last"]
]
chglist = (
[]
) # [[cycle, chg_crate, chg_cap], [cycle increase with crates and capacities for this cycle]]
dchglist = []
for i, elem in enumerate(newdf.iterrows()):
cyc = elem[1]["cycle"]
if elem[1]["type"] == "charge":
chglist.append(
[
cyc,
1 / (elem[1]["step_time_avr"] / 3600),
elem[1]["charge_avr"] * 1000,
]
)
elif elem[1]["type"] == "discharge":
dchglist.append(
[
cyc,
1 / (elem[1]["step_time_avr"] / 3600),
elem[1]["discharge_last"] * 1000 * 1000,
]
)
return chglist, dchglist
[docs]
def main():
log.setup_logging(default_level="DEBUG")
f1 = Path("../../testdata/data/20160805_test001_45_cc_01.res")
f2 = Path("../../testdata/data/20160805_test001_47_cc_01.res")
raw_files = [f1, f2]
nicknames = ["cell1", "cell2"]
logging.debug(raw_files)
logging.debug(nicknames)
ezplt = EasyPlot(raw_files, nicknames, figtitle="Test1", save_figures=True)
ezplt.plot()
plt.show()
return
def _dev_journal_loading():
log.setup_logging(default_level="DEBUG")
journal_file = Path("../../testdata/db/cellpy_batch_test.json")
ezplt = EasyPlot(
None,
journal=journal_file,
figtitle="Test1",
save_figures=False,
save_journal=True,
outpath="./tmp/",
)
ezplt.plot()
# plt.show()
return
if __name__ == "__main__":
print(" running easyplot ".center(80, "-"))
_dev_journal_loading()
print(" finished ".center(80, "-"))