Simple graphical "fitting"

Current status and purpose

Want a tool to easily plot and select points on dqdv data.
Should make it so generic that I can do the same with ocv-rlx data.

Add-ons if time allows:

  • what about some film-plots?
  • or maybe viewing the step-table to find info about the cycle?

In [ ]:


In [1]:
import os
import logging

import bokeh
import holoviews as hv
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import ipywidgets as widgets
from ipywidgets import interact, interact_manual

import cellpy
from cellpy import cellreader
from cellpy.utils import ica

%matplotlib inline
hv.extension('bokeh')

logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)

print(f"pandas: {pd.__version__}")
print(f"cellpy: {cellpy.__version__}")
print(f"holoviews: {hv.__version__}")

my_data = cellreader.CellpyData()
filename = "../../../testdata/hdf5/20160805_test001_45_cc.h5"
assert os.path.isfile(filename)
my_data.load(filename)
my_data.set_mass(0.1)


pandas: 1.0.1
cellpy: 0.3.3.a5
holoviews: 1.12.7

1. Retrieve dQ/dV data

What is the best way to retrieve dQ/dV data?


In [ ]:
comb_ica = ica.dqdv_frames(my_data, split=False, tidy=False)
comb_ica.head()

In [ ]:
cycle_number = 1
ax = comb_ica[cycle_number].plot(x="voltage")
ax.legend([f"cycle {cycle_number}"])

In [96]:
comb_ica_charge, comb_ica_discharge = ica.dqdv_frames(my_data, split=True, tidy=False, )


WARNING:root:no steps found (c:18 s:0 type:charge)
================================================================================
None
================================================================================
None

In [97]:
comb_ica_charge.head()


Out[97]:
cycle voltage 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
value v dq dq dq dq dq dq dq dq dq dq dq dq dq dq dq dq dq
0 0.107166 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1 0.116185 2378.011945 2174.907407 1938.424532 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 0.125205 2378.011945 2174.907407 1938.424532 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 0.134225 4224.043075 4397.412068 4392.859903 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 2213.724529 NaN
4 0.143244 5215.281093 5793.379548 5752.514874 2257.911912 NaN NaN NaN NaN 2453.470641 NaN NaN NaN 2171.116951 2340.668853 2249.819547 2213.724529 NaN

In [ ]:
cycle_number = 1
ax = comb_ica_charge[["voltage", cycle_number]].plot(x=("voltage", "v"))
ax.legend([f"charge cycle {cycle_number}"])

ax = comb_ica_discharge[["voltage", cycle_number]].plot(x=("voltage", "v"))
ax.legend([f"discharge cycle {cycle_number}"])

In [ ]:
tidy_ica_charge, tidy_ica_discharge = ica.dqdv_frames(my_data, split=True, tidy=True)

In [ ]:
cycle_number = 1
ax = tidy_ica_charge.loc[tidy_ica_charge.cycle == cycle_number, ["voltage", "dq"]].plot(x="voltage")
ax.legend([f"charge cycle {cycle_number}"])

Selected retrieve method

For fitting, it is most convinient to have individual datasets for charge and discharge. It is also convinient to have the data in a form where we can easily plot all the data to get an overview. And it should be easy to extract single cycles.


In [32]:
tidy_ica_charge, tidy_ica_discharge = ica.dqdv_frames(my_data, split=True, tidy=True)

cycle_number = 1
dq_charge_1 = tidy_ica_charge.loc[tidy_ica_charge.cycle == cycle_number, ["voltage", "dq"]]


WARNING:root:no steps found (c:18 s:0 type:charge)
================================================================================
None
================================================================================
None

Observed improvements needed

  • dqdv_frames gives a warning if step is not found - make that optional (warnings=False or True)
  • inconsistent column names for dqdv_frames with split=True vs split=False
    • split=True creates a "interpolated" frame with one common x-column ("voltage", "v) and one column pr cycle ("1", "dq")
    • split=False creates x,y pair values with columns ("1", "voltage") and ("1", "dq")
  • need to rename the function to something more intuitive
    • for example: make_dqdv_frame or retrieve_dqdv_frame

2. Interact and plot the data

As simple as possible


In [ ]:
pd.options.plotting.backend = "matplotlib"
ax_mplib = dq_charge_1.plot(x="voltage", style="-o")
ax_mplib.legend([f"charge cycle {cycle_number}"])

In [ ]:
pd.options.plotting.backend = "hvplot"
ax_hvplot = dq_charge_1.plot(x="voltage") * dq_charge_1.plot(x="voltage", kind="scatter")
# Unfortunately, the df.plot() method returns a hv object with a copy of the dataframe (potential memory leak)
# We therefore chose to use the hv.Curve() etc. methods directly instead.
ax_hvplot

Using holoviews directly

(since dataframe.plot() returns a copy of the data)


In [33]:
cycle_label = "Cycle 1 (charge)"
value_dims = [
    hv.Dimension("dq", label="dQ", unit="mAh/g/V")
]  # the y-axis
key_dims = [
    hv.Dimension("voltage", label="Voltage", unit="V vs. Li/Li+")
]    # the x-axis
z_dims = key_dims + value_dims

v = hv.Scatter(data=dq_charge_1, kdims=key_dims, vdims=value_dims, label=cycle_label).opts(
    width=800,
    height=400,
    marker="o",
    size=8,
    tools = ['hover']
)
vv = (v * hv.Curve(v)).opts(title="ICA plot")
vv


Out[33]:

Interactive point picking


In [34]:
import holoviews as hv
from holoviews import opts, streams
from holoviews.plotting.links import DataLink

In [90]:
dq_df = dq_charge_1
cycle_label = "Cycle 1 (charge)"
num_points = 10

value_dims = [
    hv.Dimension("dq", label="dQ", unit="mAh/g/V")
]
key_dims = [
    hv.Dimension("voltage", label="Voltage", unit="V vs. Li/Li+"),
    hv.Dimension("dq", label="dQ", unit="mAh/g/V")
]

max_dq_idx = dq_df["dq"].idxmax()
v0, dq0 = dq_df.iloc[max_dq_idx].values

peaks = pd.DataFrame({"voltage": [v0], "dq": [dq0]})
points = hv.Points(data=peaks, kdims=key_dims).opts(size=26, color="red", alpha=0.3)

v = hv.Scatter(data=dq_df, kdims=key_dims, vdims=value_dims, label=cycle_label).opts(
    width=600,
    height=400,
    marker="o",
    size=8,
    tools = ['hover']
)
vv = (v * hv.Curve(v)).opts(title="ICA plot")

point_stream = streams.PointDraw(num_objects=num_points, source=points, empty_value='black')

table = hv.Table(points, key_dims)
DataLink(points, table)

(vv * points + table).opts(
    opts.Layout(merge_tools=False),
    opts.Points(active_tools=['point_draw'], padding=0.1),
    opts.Table(editable=True))


Out[90]:

In [91]:
selected_voltage = point_stream.data["voltage"][0]
selected_icap = point_stream.data["dq"][0]
print(f"You selected ({selected_voltage:0.2}, {selected_icap:0.0f})")


You selected (0.44, 154441)

Film


In [115]:
comb_ica_charge, comb_ica_discharge = ica.dqdv_frames(my_data, split=True, tidy=False, voltage_resolution=0.02)


WARNING:root:no steps found (c:18 s:0 type:charge)
================================================================================
None
================================================================================
None

In [108]:
# should add option to dqdv_frames to set voltage resolution for interpolation

In [116]:
comb_ica_charge.columns = comb_ica_charge.columns.droplevel(1)

In [118]:
comb_ica_charge.set_index("voltage", drop=True, inplace=True)

In [119]:
comb_ica_charge.head()


Out[119]:
cycle 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
voltage
0.107166 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
0.116185 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
0.125205 3155.570486 3013.873112 2930.718128 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
0.134225 4335.882958 4327.920558 4058.993146 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
0.143244 5642.029079 6116.461669 6116.845237 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 2511.331062 NaN

In [100]:
# x-axis: "voltage"
# y-axis: cycle number
# z-axis (intensity): dq

In [294]:
img = hv.Image(
    comb_ica_charge.values[::-1], bounds=(1, 0.1, 17, 1.1)
).opts(
    #invert_xaxis=True,
    #invert_yaxis=True,
    xlabel="cycle",
    ylabel="voltage",
    ylim=(0.2, 1.0)
)
img


Out[294]:

In [ ]:


In [262]:
fig, ax = plt.subplots()
fig.set_figwidth(4)
fig.set_figheight(4)
ax.imshow(comb_ica_charge.T, 
           #aspect=0.1, 
           aspect="auto",
           interpolation="nearest", 
           origin='lower',
           extent=(0.1, 1, 1.2, 16),
          )
ax.set_xlim((0.2, 0.8))
ax.set_xlabel("Voltage (V)")
ax.set_ylabel("Cycle number")
print()