Fitting of dqdv peaks

The purpose of this notebook is to evaluate and develope a robust way of fitting dqdv data. The plan is then to implement this into the cellpy.utils.ica module (as seperate classes). It would also be valuable to equip the fitting class(es) with optional ipywidgets.


In [ ]:
%load_ext autoreload
%autoreload 2

In [ ]:
import os
import copy
import logging
import bokeh
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from cellpy import cellreader
from cellpy.utils import ica
import holoviews as hv

import ipywidgets as widgets
from ipywidgets import interact, interact_manual

%matplotlib inline
hv.extension('bokeh')

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

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)

from icafit import *

Defining dqdv peak ensambles

The natural way (and my impression is that this is how other groups also do it) of conducting an "in-depth" ica study on a LiB cell would be to measure ica of re-buildt half-cells of both the cathode and the anode of the full cells, fit the peaks of the half cells, and then a use convolution of these fits to fit the actual full cell.

Examples from the literature

Should include references here...

Plan

  1. Create a class (PeakEnsamble)
  2. Create peak ensambles by sub-classing PeakEnsamble

ToDo

  • [x] Fix so that it is possible to turn crystalline peak on and off for Si peaks
  • [ ] Set peak attributes directly
  • [ ] Make it easy (and obvious) to use previous fit-prms for new fit
  • [x] Use __add__
  • [x] Combine all fit-results into one dataframe
  • [ ] Make a ipywidget for at least one of the prms (e.g. scale)
  • [ ] Make it possible to freeze peaks and ensambles
  • [ ] Make it possible to zero out peaks?
  • [x] Fit negative of discharge curves
  • [ ] Fit discharge and charge in one go? ("hysteresis parameter")

In [ ]:


In [ ]:
logger.setLevel(logging.DEBUG)

In [ ]:
import logging
from colorama import Fore
import ipywidgets as widgets

class log_viewer(logging.Handler):
    """ Class to redistribute python logging data """

    # have a class member to store the existing logger
    logger_instance = logging.getLogger("__name__")

    def __init__(self, output=None, up_side_down=True, max_lines=20, *args, **kwargs):
        self._output = output
        self.up_side_down = up_side_down
        self.max_lines = max_lines
        if self._output is None:
            self._output = widgets.Output(layout=widgets.Layout(width='600px', height='160px', border='solid')) 
        self._output.layout.overflow_y = "scroll"
 
        # Initialize the Handler
        logging.Handler.__init__(self, *args)

        # optional take format
        # setFormatter function is derived from logging.Handler
        for key, value in kwargs.items():
            if "{}".format(key) == "format":
                self.setFormatter(value)

        # make the logger send data to this class
        self.logger_instance.addHandler(self)
        
    @property
    def output(self):
        return self._output
        

    def emit(self, record):
        """ Overload of logging.Handler method """

        record = self.format(record)
        
        if self.up_side_down:
            self.output.outputs = (
                {
                    'name': 'stdout', 
                    'output_type': 
                    'stream', 
                    'text': (Fore.BLACK + (record + '\n'))
                },
            ) + self.output.outputs[:self.max_lines]
            
        else:
            self.output.outputs = self.output.outputs[-self.max_lines:] + (
                {
                    'name': 'stdout', 
                    'output_type': 'stream', 
                    'text': (Fore.BLACK + (record + '\n'))
                },
            )

Widgets


In [ ]:
import ipywidgets as widgets
from IPython.display import display
import matplotlib.pyplot as plt
%matplotlib inline

In [ ]:
from fit_widget import SiliconPeaksFitWidget

In [ ]:
silicon = Silicon(shift=-0.0, max_point=1000000, sigma_p1=0.06)

In [ ]:
cycle = 5
cha, volt = my_data.get_ccap(cycle)
v, dq = ica.dqdv(volt, cha)

In [ ]:
silicon_fit_widget = SiliconPeaksFitWidget(silicon, cha, volt)

In [ ]:
# ---------------------------------------------------------------------------
visited = {}
options = list(range(1, 10+1))

def reset_cycle(event):
    main_logger.info("-> reset cycle")
    _load_cycle(sel.value, reset=True)
    
def load_cycle(change):
    cycle = change.new
    _load_cycle(cycle, reset=False)

def _load_cycle(cycle, reset):
    
    if not reset and cycle in visited.keys():
        c = visited[cycle]
        
    if reset and cycle in visited.keys():
        old = visited[cycle]
        v = old.x
        dq = old.y
        
        silicon = Silicon(shift=-0.0, max_point=dq.max(), sigma_p1=0.06)
        c = SiliconPeaksFitWidget(silicon, v, dq, f"Cycle {cycle}")
        visited[cycle] = c
        
    if cycle not in visited.keys():
        cha, volt = my_data.get_ccap(cycle)
        v, dq = ica.dqdv(volt, cha)
        
        silicon = Silicon(shift=-0.0, max_point=dq.max(), sigma_p1=0.06)
        c = SiliconPeaksFitWidget(silicon, v, dq, f"Cycle {cycle}")
        visited[cycle] = c
        
    with fit_window:
        fit_window.clear_output(wait=True)
        display(c)
        
def _copy(cycle1, cycle2):
    main_logger.info(f"-> copy from {cycle1} to {cycle2}")
    old = visited[cycle1]
    old_peaks = copy.deepcopy(old.peaks_object)
    cha, volt = my_data.get_ccap(cycle2)
    v, dq = ica.dqdv(volt, cha)
    new = SiliconPeaksFitWidget(old_peaks, v, dq, f"Cycle {cycle2}")
    visited[cycle2] = new
    sel.value = cycle2
        
def copy_cycle_forward(event):
    cycle1 = sel.value
    cycle2 = cycle1 + 1
    if cycle2 in options:
        _copy(cycle1, cycle2)
    else:
        main_logger.info(f"-> {cycle2} does not exist")

def copy_cycle_backward(event):
    cycle1 = sel.value
    cycle2 = cycle1 - 1
    if cycle2 in options:
        _copy(cycle1, cycle2)
    else:
        main_logger.info(f"-> {cycle2} does not exist")

fit_window = widgets.Output()

description = widgets.Label("Select cycle")
sel = widgets.Select(
    options=options,
    value=options[0],
    rows=20,
    disabled=False,
    layout=widgets.Layout(width='70%', height='220px'),
)
sel.observe(load_cycle, 'value')
reset = widgets.Button(description="Reset!")

copy_next = widgets.Button(description="Copy >>")
copy_prev = widgets.Button(description="Copy <<")

reset.on_click(reset_cycle)
copy_next.on_click(copy_cycle_forward)
copy_prev.on_click(copy_cycle_backward)

header = widgets.VBox([description, sel, reset, copy_next, copy_prev])
out = widgets.HBox([header, fit_window])

handler = log_viewer()
main_logger_out = handler.output

main_logger = logging.getLogger(__name__)
main_logger.addHandler(handler)
main_logger.setLevel(20)   # log at info level.

display(main_logger_out)
display(out)
_load_cycle(sel.value, reset=False)

Pickling test

Saving results


In [ ]:
import pickle

cc = visited[3]
c3 = cc.result
b3 = c3.best_values

pickle_out = open("example.pickle", "wb")
pickle.dump(b3, pickle_out)
pickle_out.close()

Loading results


In [ ]:
pickle_in = open("example.pickle","rb")
b3new = pickle.load(pickle_in)

In [ ]:
for key in b3new:
    cc.peaks_object.set_param(key, value=b3new[key])

Save params using dump (json)


In [ ]:
jsonstr = cc.peaks_object.params.dumps()

In [ ]:
ccc = visited[4]

In [ ]:
s = ccc.peaks_object.params.loads(jsonstr)

In [ ]:
cc.peaks_object.params

In [ ]:
ccc.peaks_object.params

Set values by writing


In [ ]:


In [ ]:
cc.set_value('shift', -0.2)

In [ ]:
cc.w_shift.value = 0.001

In [ ]:
cc.peaks_object.params.pretty_print()

In [ ]:
p_si = Silicon()
p_si.params["Si02sigma"]

In [ ]:
prm = "Si02sigma"
step = 0.01
p_si.params[prm].min += step

In [ ]:
# method two implemented in CompositeEnsamble
p_t = CompositeEnsemble(Silicon(), Graphite())
p_t.set_param('Si02sigma', minimum=0.02, vary=False)
p_t.reset_peaks()
print(f"hint: {p_t.param_hints['Si02sigma']} val: {p_t.params['Si02sigma']}")

In [ ]:


In [ ]:

Collecting all the results


In [ ]:


In [ ]:
import pandas as pd
pd.options.display.max_columns = None

In [ ]:
cycles = sorted(visited.keys())
all_dfs = []
for cycle in cycles:
    params = visited[cycle].peaks_object.params
    labels = params.keys()
    columns = list(labels)
    df = pd.DataFrame(columns=columns)
    df.loc[cycle] = None
    for label in labels:
        df.loc[cycle, label] = params[label].value
    all_dfs.append(df)
n_df = pd.concat(all_dfs)
n_df.index.name = "cycle"

In [ ]:
n_df

In [ ]:
n_df.plot(y=["Si01center", "Si02center"])

In [ ]: