Beispiel #1
0
def read_ecdh(filepath):
    """
    Author: Amund M. Raniseth
    Reads an ecdh file to a pandas dataframe
    
    .ecdh format:
        mode column: 1=Galvanostatic, 2=Linear Potential Sweep, 3=Rest

    """
    modes = {1: "Galvanostatic", 2: "Linear Potential Sweep", 3: "Rest"}

    df = pd.read_csv(filepath)

    # If it's galvanostatic we want the capacity
    mode = df['mode'].value_counts()  #Find occurences of modes
    mode = mode[
        mode.index !=
        3]  #Remove the count of modes with rest (if there are large rests, there might be more rest datapoints than GC/CV steps)
    mode = mode.idxmax()  # Picking out the mode with the maximum occurences
    LOG.debug("Found cycling mode: {}".format(modes[mode]))

    #Adding attributes must be the last thing to do since it will not be copied when doing operations on the dataframe
    df.experiment_mode = mode

    return df
Beispiel #2
0
def make_toml(folder):
    files = []
    extlist = ["mpt", "csv", "txt", "xlsx", "ecdh"]


    #Loop over all elements in current directory
    for entry in os.listdir(folder):
        # If it is a file and has the correct extension
        if os.path.isfile(entry) and entry.split(".")[-1] in extlist:
            # Add it to filelist
            files.append(entry)

    toml_str = """# "Data path", "active mass" and "nickname" (set active mass to 1 if data is normalized with regards to active mass)
files = [\n"""
    if len(files)>0:
        for file in files:
            toml_str += '\t["' + file + '","1.0"],\n'
    else:
        LOG.warning("Could not find any files in the current folder!")
        toml_str += '\t[" ","1.0"],\n'

    toml_str += "]\n\n"
    toml_str += gen_string(cfg_dict)

    with open("ecdh.toml", "w") as f:
        f.write(toml_str)
        LOG.info("Wrote example configuration to 'ecdh.toml' with %.0f files found"%len(files))
Beispiel #3
0
    def plot(self):
        if not self.plotobj:
            LOG.error(
                "No plot object supplied to the cell, but cell.plot was still called! Exiting.."
            )
            import sys
            sys.exit()

        if self.plotobj.vcplot:
            if self.df.experiment_mode == 2:
                self.edit_CV()
                self.plotobj.plot_CV(self)
            elif self.df.experiment_mode == 1:
                self.edit_GC()
                self.plotobj.plot_GC(self)

        if self.plotobj.qcplot:
            self.edit_cyclelife()
            self.plotobj.plot_cyclelife(self)

        if self.plotobj.rawplot:
            if self.plotobj.rawplot_capacity:
                self.edit_cumulative_capacity()

            self.plotobj.plot_raw(self)

        if self.plotobj.dqdvplot:
            self.edic_dQdV()
            self.plotobj.plot_dQdV(self)
Beispiel #4
0
    def edit_CV(self):
        import numpy as np
        """Takes self.df and returns self.CVdata in the format:
        self.CVdata = [cycle1, cycle2, cycle3, ... , cycleN]
        cyclex = (chg, dchg)
        chg = np.array([[v1, v2, v3, ..., vn],[i1, i2, i3, ..., in]])
        So it is an array of the voltages and corresponding currents for a charge and discharge cycle in a cycle tuple in a list of cycles. 
        """
        if self.df.experiment_mode != 2:  #If the data gathered isn't a cyclic voltammetry experiment, then this doesn't work!
            LOG.warning("File '{}' is not a CV file! It is a {} file.".format(
                self.fn, self.mode_dict[str(self.df.experiment_mode)]))
        else:
            self.CVdata = []

            #Remove all datapoints where mode != 2, we dont care about other data than CV here.
            index_names = self.df[self.df['mode'] != 2].index
            rawCVdata = self.df.drop(index_names)

            for cycle, subframe in rawCVdata.groupby('cycle number'):

                #Split into charge and discharge data
                chgdat = subframe[subframe['charge'] == True]
                dchgdat = subframe[subframe['charge'] == False]

                cycle = (np.array([chgdat['Ewe/V'], chgdat['<I>/mA']]),
                         np.array([dchgdat['Ewe/V'], dchgdat['<I>/mA']]))
                self.CVdata.append(cycle)
Beispiel #5
0
 def edit_cumulative_capacity(self):
     """This function is used when you want the cumulative capacity for plotting potential and current versus capacity in a raw data plot"""
     LOG.debug("Calculating the cumulative capcity..")
     from scipy import integrate
     cumulative_capacity = integrate.cumtrapz(abs(self.df["<I>/mA"]),
                                              self.df["time/s"] / 3600,
                                              initial=0)
     self.df[
         "CumulativeCapacity/mAh/g"] = cumulative_capacity / self.am_mass
Beispiel #6
0
def main():
    if len(sys.argv) < 2:
        run()
    elif sys.argv[1] == "init":
        make_toml("./")
    elif sys.argv[1] == "run":
        run()
    else:
        LOG.critical("Can't interpret CLI parameters.")
Beispiel #7
0
def dat_batsmall_to_vq(filename): #NOT USING PD DATAFRAME YET
    import numpy as np
    data = []
    decode_errors = 0
    decode_error_str = ""
    with open(filename, "r") as f:
        # Skip all non-data lines
        while True:
            line = f.readline()
            if '"V";I:"A";C:"Ah/kg";7' in line:
                break
        # Adding the rest of the readable file to a data array
        while True:
            try:
                line = f.readline()
                if line == '':
                    break
                else:
                    data.append(line)
            except UnicodeDecodeError as e:
                decode_errors += 1
                decode_error_str = e
                continue
        
        if decode_errors > 0:
            LOG.warning("Found %i Unicode Decode errors, thus %i lines of data has been missed. Consider getting a safer method of acquiring data. \nComplete error message: " %(decode_errors, decode_errors) + str(decode_error_str))


    charges = []
    discharges = []
    charge = True
    voltages = []
    capacities = []

    for line in data[:-1]:

        try:
            v = float(eval(line.split(";")[1]))
            q = abs(float(eval(line.split(";")[3])))
            voltages.append(v)
            capacities.append(q)
        except:
            if '"V";I:"A";C:"Ah/kg"' in line and charge == True:
                #LOG.debug("Charge end at: V: %.5f, Q: %.5f" %(v,q))
                charges.append((np.array(voltages), np.array(capacities)))
                charge = False
                voltages = []
                capacities = []
            elif '"V";I:"A";C:"Ah/kg"' in line and charge == False:
                #LOG.debug("Discharge end at: V: %.5f, Q: %.5f" %(v,q))
                discharges.append((np.array(voltages), np.array(capacities)))
                charge = True
                voltages = []
                capacities = []

    return charges, discharges
Beispiel #8
0
def check_files(list):
    # Checks that files exist and then returns the ones that does.
    return_list = []
    for file in list:
        filename = file[0]
        if os.path.isfile(filename):
            return_list.append(file)
        else:
            LOG.error(
                "File not found: '" + str(filename) +
                "' Please check that the correct path is typed in your input toml file. Skipping this file."
            )
    return return_list
Beispiel #9
0
def read_config(path):
    try:
        toml_str = open(path, "r").read()
    except Exception as e:
        LOG.error(f"Couldn't read config file: {e}")
        import sys
        sys.exit()
        
    # Check toml string
    check_config(toml_str)
    # Fill toml config
    config = toml.loads(toml_str)
    config = fill_config(config)
    return config
Beispiel #10
0
    def plot_raw(self, cellobj):
        """Takes a cellobject and plots it in a raw data plot (I/mA and Ewe/V vs time/s)"""
        LOG.debug("Running plot.py plot_raw")
        # Get subplot from main plotclass
        if self.all_in_one is False:
            ax = self.give_subplot()
            ax.set_title(cellobj.name)
            #Initiate twin ax
            ax2 = ax.twinx()
            #self.axes.append(ax2)
        else:
            if self.qcplot and self.rawplot:
                ax = self.axes[2]
            elif self.all_in_one and not self.qcplot:
                ax = self.axes[0]
            elif self.all_in_one and self.qcplot:
                ax = self.axes[1]
            elif self.qcplot or self.rawplot:
                ax = self.axes[1]
            else:
                ax = self.axes[0]
            #Initiate twin axis, only if it doesnt exist aleady
            if not self.axtwinx:
                self.axtwinx = ax.twinx()
            ax2 = self.axtwinx

            ax.set_title(" ")

        #if specific cycles, then remove all other cycles
        if cellobj.specific_cycles:
            df = cellobj.df[cellobj.df['cycle number'].isin(
                cellobj.specific_cycles)]
        else:
            df = cellobj.df

        #Placing it in a plot
        if self.rawplot_capacity:
            x = df["CumulativeCapacity/mAh/g"]
            ax.set_xlabel(r"Cumulative Capacity [$\frac{mAh}{g}$]")
        else:
            x = df["time/s"] / 3600

            ax.set_xlabel("Time [h]")

        ax.plot(x, df["Ewe/V"], color=cellobj.color, label=cellobj.name)
        ax2.plot(x, df["<I>/mA"], color=cellobj.color, linestyle="dotted")

        ax.set_ylabel("Potential [V]")
        ax2.set_ylabel("Current [mA]")
Beispiel #11
0
    def plot_CV(self, cellobj):
        """Takes a cellobject and plots it in a CV plot (I/mA vs Ewe/V)"""
        LOG.debug("Running plot.py plot_CV")
        # Get subplot from main plotclass
        if self.all_in_one is False:
            ax = self.give_subplot()
            ax.set_title(f"{cellobj.name}")
        else:
            ax = self.axes[0] if self.qcplot is False else self.axes[1]
            ax.set_title(" ")  # Cyclic Voltammograms

        #Placing it in a plot with correct colors
        self.insert_cycle_data(cellobj, ax, cellobj.CVdata)

        ax.set_ylabel("Current [mA]")
        ax.set_xlabel("Potential [V]")
Beispiel #12
0
    def plot_GC(self, cellobj):
        """Takes a cell dataframe and plots it in a GC plot (Ewe/V vs Cap)"""
        LOG.debug("Running plot.py plot_GC")

        # Get subplot from main plotclass
        if self.all_in_one is False:
            ax = self.give_subplot()
            ax.set_title(f"{cellobj.name}")
        else:
            ax = self.axes[0] if self.qcplot is False else self.axes[1]
            ax.set_title("")  #Galvanostatic Cycling

        #Placing it in a plot with correct colors
        self.insert_cycle_data(cellobj, ax, cellobj.GCdata)

        ax.set_xlabel(r"Capacity [$\frac{mAh}{g}$]")
        ax.set_ylabel("Potential [V]")
Beispiel #13
0
    def plot_dQdV(self, cellobj):
        """Takes a cell dataframe and plots it in a dQdV plot (dQdV/mAhVg vs Ewe/V)"""
        LOG.debug("Running plot.py plot_dQdV")

        # Get subplot from main plotclass
        if self.all_in_one is False:
            ax = self.give_subplot()
            ax.set_title("dQdV: {}".format(os.path.basename(cellobj.fn)))
        else:
            ax = self.axes[0] if self.qcplot is False else self.axes[1]
            ax.set_title("dQdV")

        #Placing it in a plot with correct colors
        self.insert_cycle_data(cellobj, ax, cellobj.dQdVdata)

        ax.set_ylabel(r"dQ/dV [$log\left(\frac{mAh}{Vg}\right)$ smoothed]")
        ax.set_xlabel("Potential [V]")
Beispiel #14
0
    def edit_cyclelife(self):
        import numpy as np
        import pandas as pd
        #First make sure that we have either CV capacity data or galvanostatic
        if self.df.experiment_mode == 2:
            if not self.CVdata_capacity:
                LOG.error(
                    "in cell/edit_cyclelife, CVdata_capacity has not been made."
                )
                self.edit_CV_capacity()

        elif self.df.experiment_mode == 1:
            #If the GC data doesn't exist, make it.
            if not self.GCdata:
                self.edit_GC()

            #Make temporary dataholders
            tmpdat = []
            #loop through data and gather capacities
            for i, cycle in enumerate(self.GCdata):
                chg, dchg = cycle
                try:
                    tmpdat.append([i, chg[0][-1], dchg[0][-1]])
                except:
                    try:
                        tmpdat.append([i, chg[0][-1], 0])
                    except:
                        try:
                            tmpdat.append([i, 0, dchg[0][-1]])
                        except:
                            tmpdat.append([i, 0, 0])

            self.cyclelifedata = pd.DataFrame(tmpdat,
                                              columns=[
                                                  "cycle",
                                                  "charge capacity/mAh",
                                                  "discharge capacity/mAh"
                                              ])

            self.cyclelifedata["coulombic efficiency"] = self.cyclelifedata[
                "discharge capacity/mAh"] / self.cyclelifedata[
                    "charge capacity/mAh"] * 100
Beispiel #15
0
    def smooth_data(self, datatreatment):
        """
        Takes the data and removes outliers
        Useful if you have a couple of short circuits now and then
        
        """
        LOG.info(f"Removing outliers on {self.name}.")
        import pandas as pd
        window = int(len(self.df) / 10000)
        smoothing_df = pd.DataFrame()
        smoothing_df['median'] = self.df['Ewe/V'].rolling(window).median()
        smoothing_df['std'] = self.df['Ewe/V'].rolling(window).std()

        expmode = self.df.experiment_mode
        #filter setup
        self.df = self.df[(self.df['Ewe/V'] <= smoothing_df['median'] +
                           3 * smoothing_df['std'])
                          & (self.df['Ewe/V'] >= smoothing_df['median'] -
                             3 * smoothing_df['std'])].ewm(alpha=0.9).mean()
        self.df.experiment_mode = expmode
Beispiel #16
0
def read(filepath):
    """
    Takes a filepath, finds out what file type it is, and reads it into two list of lists, fix this! maybe Cell object? or pd array?"""
    # Check input file and create proper data thereafter
    fn, ext = os.path.splitext(filepath)
    LOG.debug(f"Reading file: '{filepath}'")
    if ext == ".xlsx":
        from ecdh.readers import Neware as NA
        df = NA.read_xlsx(filepath)
    elif ext == ".csv":
        from ecdh.readers import Neware as NA
        df = NA.read_csv(
            filepath
        )  #but this gives nested list with V/q data for each cycle.
    elif ext == ".mpt":
        from ecdh.readers import BioLogic as BL
        df = BL.read_mpt(filepath)
    elif ext == ".txt":
        from ecdh.readers import BatSmall as BS
        df = BS.read_txt(filepath)
    elif ext == ".ecdh":
        from ecdh.readers import Processed as PC
        df = PC.read_ecdh(filepath)
    else:
        LOG.error(f"File format not supported: {ext}")
        LOG.error("Exiting..")
        exit()

    return df
Beispiel #17
0
def gen_string(cfg_dict):
    masterstr = ""
    for key, item in cfg_dict.items():
        masterstr += "[" + key + "]\n"
        lines = []
        for subkey, subitem in item.items():
            if type(subitem[0]) is bool or type(subitem[0]) is float or type(subitem[0]) is int:
                kwarg_default_value = str(subitem[0]).lower()
            elif type(subitem[0]) is str:
                kwarg_default_value = "'" + str(subitem[0]) + "'"
            else:
                LOG.debug("Keyword in cfg_dict conf.py found to be neither str nor bool nor float nor int. This needs special handling.")
                kwarg_default_value = "false"

            left_align = subkey + " = " + kwarg_default_value
            center_align = "# " + subitem[1]
            lines.append(f"{left_align : <30} {center_align : ^30}")

        for line in lines:
            masterstr += line + "\n"
        masterstr += "\n"
    return masterstr
Beispiel #18
0
    def reduce_data(self, datatreatment):
        """
        Takes the data and deletes points not matching the requirement.
        Useful if you measured with too high sampling rate and have gigantic datafiles
        Example: you sampled for every 1mV change, and every second, then with dV = 0.010 and dt = 10, all points that has less than a difference of 10mV AND less than 10s will be deleted.
        """
        LOG.debug("cell.reduce_data() is running, reducing file: {}".format(
            self.name))
        LOG.info(
            "Reducing file: {}. Follow loading bar below, this might take some time."
            .format(self.name))

        try:
            dt = int(datatreatment["dt"])
        except:
            dt = 10
        try:
            dV = float(datatreatment["dV"])
        except:
            dV = 0.01

        #dt = 10 #10 s
        #dV = 0.01 # 10mV
        dI = 0.01  #0.01 mA, 10uA
        LOG.debug(f"Reduction parameters: dt: {dt}, dV: {dV}, dI: {dI}.")
        #'time/s','Ewe/V', '<I>/mA'
        last_t = self.df['time/s'].iloc[0]
        last_V = self.df['Ewe/V'].iloc[0]
        kill_index = []

        max_i = len(self.df)
        loadbar = 10
        print("|----------|")
        print("|", end="", flush=True)
        for i, row in self.df.iloc[1:].iterrows():

            curr_t = row.iloc[0]
            curr_V = row.iloc[1]

            if abs(last_t - curr_t) < dt and abs(last_V - curr_V) < dV:
                kill_index.append(i)
            else:
                last_t = curr_t
                last_V = curr_V

            if i == int(max_i / loadbar):
                print("-", end="", flush=True)
                loadbar -= 1
                if loadbar < 1:
                    loadbar += 1  #Wow this needs fixing
        print("|")

        filename, ext = os.path.splitext(self.fn)
        self.df.drop(kill_index, inplace=True)
        self.df.to_csv(path_or_buf=filename + "_reduced.ecdh")
Beispiel #19
0
 def treat_data(self, config):
     LOG.debug("Treating data")
     if config["start_cut"]:
         start_cut = int(config["start_cut"])
         # Trimming cycles
         if start_cut > len(self.discharges) or self.start_cut > len(
                 self.charges):
             LOG.warning(
                 f"Start cut is set to {self.start_cut} but the number of discharges is only {len(self.discharges)}. Cuts will not be made."
             )
         else:
             LOG.debug("Cutting off start cycles")
             self.discharges = self.discharges[self.start_cut:]
             self.charges = self.charges[self.start_cut:]
Beispiel #20
0
def read_txt(filepath):
    """
    Author: Amund M. Raniseth
    Reads a txt file to a pandas dataframe

    .txt format:
        line 1: user comment
        header: includes columns TT, U, I, Z1 ++ , corresponding to time, potential, current and cycle number

    Important: In the datafile, in the first line (which is the user description) you must use the keyword CV or GC to tell what type of data it is."""
    import pandas as pd
    import numpy as np
    import os
    import gc

    # Open file
    with open(filepath, 'r') as f:
        lines = f.readlines()

    # Find out what type of experiment this is
    expmode = 0
    for line in lines:
        if "CV" in line or "Cyclic Voltammetry" in line or "cyclic voltammetry" in line:
            expmode = 2
            break
        elif "GC" in line or "Galvanostatic" in line or "galvanostatic" in line:
            expmode = 1
            break
        else:
            LOG.warning("Cannot find out which type of experiment the file {} is! Please specify either CV or GC in the comment on the top of the file.".format(os.path.basename(filepath)))
            break

    # Find out how many headerlines this file have
    headerlines = 0
    for i,line in enumerate(lines):
        if 'TT' in line and 'U ' in line and 'I ' in line:
            headerlines = i
            break

    # Read all data to a pandas dataframe
    try:
        big_df = pd.read_csv(filepath, header = headerlines-1, sep = "\t")
    except UnicodeDecodeError as e:
        LOG.debug(f"Ran in to UnicodeDecodError in readers/BatSmall.py, reading with ANSI encoding. Error: {e}")
        big_df = pd.read_csv(filepath, header = headerlines-1, sep = "\t", encoding = 'ANSI')
    
    #Extract useful columns, change the name of the columns, make all numbers numbers.
    def _which_cap_col(big_df):
        for col in big_df.columns:
            if 'C [mAh/kg]' in col:
                return 'C [mAh/kg]'
            elif 'C [Ah/kg]' in col:
                return 'C [Ah/kg]'

    def _which_time_col(big_df):
        for col in big_df.columns:
            if 'TT [h]' in col:
                return 'TT [h]'
            elif 'TT [s]' in col:
                return 'TT [s]'


    # Drop rows with comments as these are either change of program and have duplicate data or may be EIS type data
    big_df.drop(big_df.dropna(subset=['Comment']).index, inplace= True)
    big_df.reset_index(inplace=True)


    df = big_df[[_which_time_col(big_df), 'U [V]', 'I [mA]', 'Z1 []', _which_cap_col(big_df)]]
    del big_df #deletes the dataframe
    gc.collect() #Clean unused memory (which is the dataframe above)

    # Transform units of colums
    if 'C [mAh/kg]' in df.columns:
        df['C [mAh/kg]'] = df['C [mAh/kg]'].apply(lambda x: abs(x/1000)) #Convert from mAh/kg to Ah/kg which is equal to mAh/g
    if 'TT [h]' in df.columns:
        df['TT [h]'] = df['TT [h]'].apply(lambda x: x*3600) #Converting from h to s
    
    # Rename columns and assure datatype
    df.columns = ['time/s','Ewe/V', '<I>/mA', 'cycle number', 'capacity/mAhg'] #Renaming the columns. columns={'TT [h]': 'time/s', 'U [V]': 'Ewe/V', 'I [mA]': '<I>/mA', 'Z1 []':'cycle number', 'C [mAh/kg]':'capacity/mAhg'}, inplace=True)
    df = df.astype({"time/s": float, "Ewe/V": float, "<I>/mA": float, "capacity/mAhg": float, "cycle number": int})

    df['mode'] = expmode
    df['charge'] = True
    df.experiment_mode = expmode
    df.name = os.path.basename(filepath)

    

    
    check_df(df)

    df = clean_df(df)


    return df
Beispiel #21
0
    def insert_cycle_data(self, cellobj, ax, data):
        """
        Inserts the given data in the given axis with data from the cellobject
        Data must be on form 
        data = [cycle1, cycle2, cycle3, ... , cycleN]
        cyclex = (chg, dchg)
        chg = np.array([[x1, x2, x3, ..., xn],[y1, y2, y3, ..., yn]]) where x is usually either capacity or potential and y usually potential or current
        """
        # Generating colormap
        cmap = self.colormap(
            cellobj.color)  #create colormap for fade from basic color
        #Define cycle amount for use with colors
        Nc = len(data)

        # Plot it
        ## count number of specific cycles
        if cellobj.specific_cycles:
            numcyc = len(cellobj.specific_cycles)
        else:
            numcyc = 0

        if numcyc > 8 and Nc > 8:  #Use colorbar if more than 4 cycles and no specific cycles.
            LOG.debug(f"numcyc: {numcyc}, Nc: {Nc}")
            if cellobj.specific_cycles:
                maxcyc = max(cellobj.specific_cycles)
                for i, cycle in enumerate(data):
                    if i in cellobj.specific_cycles:
                        chg, dchg = cycle
                        ax.plot(chg[0], chg[1], color=cmap(i / maxcyc))
                        ax.plot(dchg[0], dchg[1], color=cmap(i / maxcyc))
            else:
                for i, cycle in enumerate(data):
                    chg, dchg = cycle
                    ax.plot(chg[0], chg[1], color=cmap(i / Nc))
                    ax.plot(dchg[0], dchg[1], color=cmap(i / Nc))

            # Adding colorbar to plot
            if cellobj.specific_cycles:
                maxcyc = max(cellobj.specific_cycles)
            else:
                maxcyc = Nc
            sm = plt.cm.ScalarMappable(cmap=cmap,
                                       norm=plt.Normalize(vmin=0, vmax=maxcyc))
            sm._A = []
            if self.all_in_one is True:
                self.fig.colorbar(sm,
                                  ax=ax,
                                  label="Cycle number for {}".format(
                                      cellobj.name))
            else:
                self.fig.colorbar(sm, ax=ax, label="Cycle number")

        else:  #There are either specific cycles or <=5 cycles in the data

            colorlist = [
                'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple',
                'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan'
            ]
            #colorlist = self.colors
            if cellobj.specific_cycles:
                j = -1
                customlabel = ["C/10", "C/5", "C/2", "C", "2C", "5C"]
                for i, cycle in enumerate(data):
                    if i in cellobj.specific_cycles:
                        j += 1
                        color = colorlist[0]
                        colorlist = colorlist[1:]

                        chg, dchg = cycle
                        if self.all_in_one is True:
                            ax.plot(chg[0],
                                    chg[1],
                                    color=color,
                                    label=f"{cellobj.name} Cycle {i}"
                                    )  #This is the charge cycle
                        else:
                            ax.plot(chg[0],
                                    chg[1],
                                    color=color,
                                    label="Cycle {}".format(
                                        i))  #This is the charge cycle
                            #ax.plot(chg[0], chg[1], color = color, label = customlabel[j]) #This is the charge cycle with a custom label specified before this if
                        ax.plot(dchg[0], dchg[1], color=color)  #1 is discharge
            else:  # Is this actually ever triggered anymore with the new specific cycles with cycle ranges?
                for i, cycle in enumerate(data):

                    color = colorlist[0]
                    colorlist = colorlist[1:]

                    chg, dchg = cycle
                    if self.all_in_one is True:
                        ax.plot(chg[0],
                                chg[1],
                                color=color,
                                label=f"{cellobj.name} Cycle {i}"
                                )  #This is the charge cycle
                    else:
                        ax.plot(chg[0],
                                chg[1],
                                color=color,
                                label="Cycle {}".format(
                                    i))  #This is the charge cycle
                    ax.plot(dchg[0], dchg[1], color=color)  #1 is discharge
Beispiel #22
0
    def __init__(self, numfiles=1, **kwargs):
        for key in kwargs:
            setattr(self, key, kwargs[key])
        self.taken_subplots = 0

        # Finding number of subplots
        if self.qcplot is True:
            self.subplots = 1
        else:
            self.subplots = 0

        if self.all_in_one:
            if self.vcplot:
                self.subplots += 1
            if self.rawplot:
                self.subplots += 1
            if self.dqdvplot:
                self.subplots += 1
        else:
            if self.vcplot is True:
                self.subplots += numfiles

            if self.dqdvplot is True:
                self.subplots += numfiles

            if self.rawplot:
                self.subplots += numfiles

        # List of available colors

        if numfiles > 10:
            tableau20 = [(31, 119, 180), (174, 199, 232), (255, 127, 14),
                         (255, 187, 120), (44, 160, 44), (152, 223, 138),
                         (214, 39, 40), (255, 152, 150), (148, 103, 189),
                         (197, 176, 213), (140, 86, 75), (196, 156, 148),
                         (227, 119, 194), (247, 182, 210), (127, 127, 127),
                         (199, 199, 199), (188, 189, 34), (219, 219, 141),
                         (23, 190, 207), (158, 218, 229)]
            for i in range(len(tableau20)):
                r, g, b = tableau20[i]
                tableau20[i] = (r / 255., g / 255., b / 255.)
            self.colors = tableau20
            LOG.warning(
                "You have chosen to plot more than 10 files, which will look messy, but you do you."
            )
        else:
            tableau10 = [
                'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple',
                'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan'
            ]
            self.colors = tableau10
        # Initiate figure and axes
        if self.subplots > 2:  # Then two columns, or more.
            rows = int(sqrt(self.subplots))
            cols = ceil(self.subplots / rows)
            self.fig, self.axes = plt.subplots(nrows=rows, ncols=cols)
            self.axes = self.axes.reshape(-1)
        else:
            self.fig, self.axes = plt.subplots(ncols=self.subplots)
        if self.suptitle:
            self.fig.suptitle(self.suptitle)
        else:
            self.fig.suptitle(str(date.today()))

        self.fig.tight_layout()

        #Make sure self.axes is a list if it is only 1 element
        try:
            iter(self.axes)
        except:
            self.axes = [self.axes]

        for ax in self.axes:
            #ax.figure.set_size_inches(8.4, 4.8, forward = True)
            ax.set(
                title='Generic subplot title',
                ylabel='Potential [V]',
                xlabel='Specific Capacity [mAh/g]',
                #ylim = (2.5,5),
                #xlim = (0, 150),
                #xticks = (np.arange(0, 150), step=20)),
                #yticks = (np.arange(3, 5, step=0.2)),
            )
            ax.tick_params(direction='in', top='true', right='true')

        # If cycle life is to be plotted: Make the first subplot this.
        if self.qcplot == True:
            self.taken_subplots += 1
            # Dealing with percentage
            if self.percentage == True:
                if self.ylabel:
                    ylabel = self.ylabel
                else:
                    ylabel = 'Capacity retention [%]'
                self.axes[0].yaxis.set_major_formatter(
                    mtick.PercentFormatter(xmax=1, decimals=0))
            else:
                if self.ylabel:
                    ylabel = self.ylabel
                else:
                    ylabel = 'Specific capacity [mAh/g]'

            self.axes[0].set(title='Cycle life' if len(self.axes) != 1 else '',
                             ylabel=ylabel,
                             xlabel='Cycles')

        if self.rawplot and self.all_in_one:
            self.axtwinx = None
Beispiel #23
0
    def draw(self, save=False, show=True):
        LOG.debug("Running plot.py draw()")
        if self.qcplot == True:
            # Get labels and handles for legend generation and eventual savefile
            handles, labels = self.axes[0].get_legend_handles_labels()
            handles.append(
                Line2D([0], [0],
                       marker='o',
                       color='black',
                       alpha=0.2,
                       label='Charge capacity',
                       linestyle=''))
            if self.coulombicefficiency:
                handles.append(
                    Line2D([0], [0],
                           marker='+',
                           color='black',
                           alpha=0.2,
                           label='Coulombic Efficiency',
                           linestyle=''))
            try:
                loc = self.legend_location
            except Exception as e:
                loc = 'best'
                LOG.debug(
                    f"Plot.py draw(), error with legend_location setting: {e}")
            self.axes[0].legend(handles=handles, loc=loc)
            if type(self.specific_cycles) != bool:
                #self.axes[0].scatter(self.specific_cycles, np.zeros(len(self.specific_cycles)), marker = "|")
                x = 0
            # Title also has to be adjusted

        #if self.rawplot == True:
        #self.fig.suptitle("Cyclic voltammetry")
        #for ax in self.axes:
        #    ax.set_title("")

        # Makes more space.
        #self.fig.subplots_adjust(hspace=0.4, wspace=0.4)

        if self.all_in_one is True:
            plt.legend(loc=self.legend_location)

        try:
            if len(self.specific_cycles) < 9:
                for ax in self.axes:
                    ax.legend(loc=self.legend_location)
        except Exception as e:
            LOG.debug(f"Something wrong in plot.py draw(): {e}")

        # Save if True, If out path specified, save there.
        if save == True:
            """ Fix this sometime
            handles, labels = self.axes[0].get_legend_handles_labels()
            savename = "CapRet"
            for label in labels:
                savename += "_" + label"""
            plt.savefig("ecdhfig.png", bbox_inches='tight', dpi=500)
        elif type(save) == str:
            plt.savefig(save, bbox_inches='tight')

        if show == True:
            #plt.legend(loc='lower left')
            plt.show()
Beispiel #24
0
def run():
    import sys
    if len(sys.argv) < 3: #Then no folder is specified, look for toml in local folder.
        if os.path.isfile("./ecdh.toml"):
            path = "./ecdh.toml"
        else:
            LOG.error("Could not find an ecdh.toml file in the current directory! \nOptions: \n1. Run ecdh with the argument 'run' followed by the path of your .toml file. \n2. Initiate toml file in this directory with the init argument.")
            sys.exit()
    elif os.path.isfile(sys.argv[2]):# File was inserted Read toml config
        path = sys.argv[2]
    else:
        LOG.error("Cannot find the .toml configuration file!")
        sys.exit()
    LOG.debug("Reading config file: '{}'".format(path))

    # Read in configuration file
    config = read_config(path)
    settings = config["settings"]
    # Merge cycle range into specific cycles
    if settings['cycle_range']:
        try:
            cyclerange = np.linspace(settings['cycle_range'][0], settings['cycle_range'][1], settings['cycle_range'][1]-settings['cycle_range'][0] + 1).astype(int)
            if type(settings['specific_cycles']) is bool:
                settings['specific_cycles'] = cyclerange.tolist()
            else:
                settings['specific_cycles'] += cyclerange.tolist()
            LOG.info(f"Specific cycles: {settings['specific_cycles']}")
        except Exception as e:
            LOG.warning(f"Could not use the cycle range, Error: {e}")


    datatreatment = config["datatreatment"]

    # Check that files are found
    files = check_files(config["files"])
    if len(files) == 0:
        import sys
        LOG.error("Could not load any datafiles. Exiting. Check that the filepaths are typed correctly in the configuration file.")
        sys.exit()
    LOG.success("Running ECDH: Found {} datafiles!".format(len(files)))

    # Define plot specifications
    plot = Plot(numfiles=len(files), **settings)


    # Run the data reading + plot generation
    cells = []
    for f in files:
        try:
            am_mass = f[1]
        except:
            am_mass = None
        try:
            nickname = f[2]
        except:
            nickname = None

        cell = Cell(f[0], am_mass, nickname,  plot=plot, specific_cycles = settings['specific_cycles'])
        cell.get_data()
        #cell.edit_GC()
        #cell.treat_data(settings)
        cell.plot()
        #cells.append(cell)
        
        if datatreatment['reduce_data']:
            cell.reduce_data(datatreatment)

        if datatreatment['smooth_data']:
            cell.smooth_data(datatreatment)
        
        if datatreatment['print_capacities']:

            if not os.path.isfile("capacity_intervals.json"):
                with open("capacity_intervals.json", "w") as f:
                    f.close()

            import json
 
            new_json = cell.get_capacities(datatreatment)

            with open("capacity_intervals.json",'r+') as file:
                # First we load existing data into a dict.
                try:
                    file_data = json.load(file)
                except:
                    file_data = []
                # Join new_data with file_data inside emp_details
                file_data.append(new_json)
                # Sets file's current position at offset.
                file.seek(0)
                # convert back to json.
                json.dump(file_data, file, indent = 4)
            
            
            
        


    if 'savefig' in settings:
        plot.draw(save = settings['savefig'])
    else:
        plot.draw()
Beispiel #25
0
 def get_data(self):
     # Read input file
     self.df = readers.read(self.fn)
     LOG.debug("Data has been read successfully")
Beispiel #26
0
# -*- coding: utf-8 -*-

"""This file is ran when user calls ecdh"""

from ecdh.log import LOG
LOG.set_level("DEBUG")
from ecdh.readers import check_files

from ecdh.conf import read_config, make_toml
from ecdh.plot import *
from ecdh.cell import *


import sys

def run():
    import sys
    if len(sys.argv) < 3: #Then no folder is specified, look for toml in local folder.
        if os.path.isfile("./ecdh.toml"):
            path = "./ecdh.toml"
        else:
            LOG.error("Could not find an ecdh.toml file in the current directory! \nOptions: \n1. Run ecdh with the argument 'run' followed by the path of your .toml file. \n2. Initiate toml file in this directory with the init argument.")
            sys.exit()
    elif os.path.isfile(sys.argv[2]):# File was inserted Read toml config
        path = sys.argv[2]
    else:
        LOG.error("Cannot find the .toml configuration file!")
        sys.exit()
    LOG.debug("Reading config file: '{}'".format(path))

    # Read in configuration file
Beispiel #27
0
 def edit_CV_capacity(self):
     import numpy as np
     LOG.error(
         "Cell.py/edit_CV_capacity has not been made! Creating data with only zeros."
     )
     self.CVdata_capacity = [(np.array([[0], [0]]), np.array([[0], [0]]))]
Beispiel #28
0
    def edit_GC(self):
        import numpy as np
        import pandas as pd
        """Takes self.df and returns self.GCdata in the format:
        self.GCdata = [cycle1, cycle2, cycle3, ... , cycleN]
        cyclex = (chg, dchg)
        chg = np.array([[q1, q2, q3, ..., qn],[v1, v2, v3, ..., vn]]) where q is capacity"""
        if self.df.experiment_mode != 1:  #If the data gathered isn't a galvanostatic experiment, then this doesn't work!
            LOG.warning("File '{}' is not a GC file! It is a {} file.".format(
                self.fn, self.mode_dict[str(self.df.experiment_mode)]))
        else:
            self.GCdata = []

            #Remove all datapoints where mode != 1, we dont care about other data than GC here.
            index_names = self.df[self.df['mode'] != 1].index
            rawGCdata = self.df.drop(index_names)

            for cycle, subframe in rawGCdata.groupby('cycle number'):

                #Split into charge and discharge data
                chgdat = subframe[subframe['charge'] == True].copy(deep=True)
                dchgdat = subframe[subframe['charge'] == False].copy(deep=True)
                #print(subframe.head)

                if self.am_mass or 'capacity/mAhg' not in self.df.columns:
                    if not self.am_mass:
                        self.am_mass = 1
                    #The inserted Active material mass might differ from the one the instrument software calculated. Thus we make our own capacity calculations.
                    from scipy import integrate
                    #Integrate current over time, returns mAh, divide by active mass to get gravimetric
                    #Placed inside try/except because sometimes the lenght of chgdat["<I>/mA"] or dch is 0 (when the cycle has started but the second redox mode hasnt started), then cumtrapz will fail.
                    try:
                        chgdat.loc[:, 'capacity/mAhg'] = integrate.cumtrapz(
                            abs(chgdat["<I>/mA"]),
                            chgdat["time/s"] / 3600,
                            initial=0) / self.am_mass

                    except Exception as e:
                        LOG.debug(
                            f"something went wrong with the scipy.integrate.cumtrapz in cell.py under edit_GC: {e}"
                        )
                        if not chgdat.empty:
                            chgdat.loc[:, 'capacity/mAhg'] = 0
                        else:
                            chgdat['capacity/mAhg'] = []

                    try:
                        dchgdat.loc[:, 'capacity/mAhg'] = integrate.cumtrapz(
                            abs(dchgdat["<I>/mA"]),
                            dchgdat["time/s"] / 3600,
                            initial=0) / self.am_mass
                    except:
                        if not dchgdat.empty:
                            dchgdat.loc[:, 'capacity/mAhg'] = 0
                        else:
                            dchgdat['capacity/mAhg'] = []

                cycle = (np.array([chgdat['capacity/mAhg'], chgdat['Ewe/V']]),
                         np.array([dchgdat['capacity/mAhg'],
                                   dchgdat['Ewe/V']]))
                self.GCdata.append(cycle)

            if self.plotobj.hysteresisview:
                #Then it should be plottet hysteresis-style
                last_cycle_capacity = 0
                for cycle in self.GCdata:
                    #offsett all the capacity by the last cycles last capacity
                    #cycle[1][0] += last_cycle_capacity
                    #make the discharge capacity negative and offset it so it starts from the last charge capacity
                    cycle[1][0] *= -1
                    cycle[1][0] += cycle[0][0][-1]
                    #Update the variable so the next cycle is offset by the correct amount
                    try:
                        last_cycle_capacity = cycle[1][0][-1]
                    except Exception as e:
                        LOG.debug(
                            f"cell.py edit_GC if hysteresisview: couldn't get the last capacity element of the discharge: {e}"
                        )
Beispiel #29
0
def read_mpt(filepath):
    """
    Author: Amund M. Raniseth
    Reads an mpt file to a pandas dataframe
    
    .MPT format:
        mode column: 1=Galvanostatic, 2=Linear Potential Sweep, 3=Rest

    """
    modes = {1: "Galvanostatic", 2: "Linear Potential Sweep", 3: "Rest"}

    #with open(filepath, 'r', encoding= "iso-8859-1") as f:  #open the filepath for the mpt file
    #    lines = f.readlines()
    with open(filepath, errors='ignore') as f:
        lines = f.readlines()

    # now we skip all the data in the start and jump straight to the dense data
    headerlines = 0
    for line in lines:
        if "Nb header lines :" in line:
            headerlines = int(line.split(':')[-1])
            break  #breaks for loop when headerlines is found

    for i, line in enumerate(lines):
        #You wont find Characteristic mass outside of headerlines.
        if headerlines > 0:
            if i > headerlines:
                break

        if "Characteristic mass :" in line:
            active_mass = float(line.split(':')[-1][:-3].replace(',',
                                                                 '.')) / 1000
            LOG.debug("Active mass found in file to be: " + str(active_mass) +
                      "g")
            break  #breaks loop when active mass is found

    # pandas.read_csv command automatically skips white lines, meaning that we need to subtract this from the amout of headerlines for the function to work.
    whitelines = 0
    for i, line in enumerate(lines):
        #we dont care about outside of headerlines.
        if headerlines > 0:
            if i > headerlines:
                break
        if line == "\n":
            whitelines += 1

    #Remove lines object
    del lines
    gc.collect()

    big_df = pd.read_csv(filepath,
                         header=headerlines - whitelines - 1,
                         sep="\t",
                         encoding="ISO-8859-1")
    LOG.debug("Dataframe column names: {}".format(big_df.columns))

    # Start filling dataframe
    if 'I/mA' in big_df.columns:
        current_header = 'I/mA'
    elif '<I>/mA' in big_df.columns:
        current_header = '<I>/mA'
    df = big_df[[
        'mode', 'time/s', 'Ewe/V', current_header, 'cycle number', 'ox/red'
    ]]
    # Change headers of df to be correct
    df.rename(columns={current_header: '<I>/mA'}, inplace=True)

    # If it's galvanostatic we want the capacity
    mode = df['mode'].value_counts()  #Find occurences of modes
    mode = mode[
        mode.index !=
        3]  #Remove the count of modes with rest (if there are large rests, there might be more rest datapoints than GC/CV steps)
    mode = mode.idxmax()  # Picking out the mode with the maximum occurences
    LOG.debug("Found cycling mode: {}".format(modes[mode]))

    if mode == 1:
        df = df.join(big_df['Capacity/mA.h'])
        df.rename(columns={'Capacity/mA.h': 'capacity/mAhg'}, inplace=True)
        if df.dtypes[
                'capacity/mAhg'] == str:  #the str.replace only works and is only needed if it is a string
            df['capacity/mAhg'] = pd.to_numeric(
                df['capacity/mAhg'].str.replace(',', '.'))
    del big_df  #deletes the dataframe
    gc.collect()  #Clean unused memory (which is the dataframe above)
    # Replace , by . and make numeric from strings. Mode is already interpreted as int.
    for col in df.columns:
        if df.dtypes[
                col] == str:  #the str.replace only works and is only needed if it is a string
            df[col] = pd.to_numeric(df[col].str.replace(',', '.'))
    #df['time/s'] = pd.to_numeric(df['time/s'].str.replace(',','.'))
    #df['Ewe/V'] = pd.to_numeric(df['Ewe/V'].str.replace(',','.'))
    #df['<I>/mA'] = pd.to_numeric(df['<I>/mA'].str.replace(',','.'))
    #df['cycle number'] = pd.to_numeric(df['cycle number'].str.replace(',','.')).astype('int32')
    df.rename(columns={'ox/red': 'charge'}, inplace=True)
    df['charge'].replace({1: True, 0: False}, inplace=True)

    if mode == 2:
        # If it is CV data, then BioLogic counts the cycles in a weird way (starting new cycle when passing the point of the OCV, not when starting a charge or discharge..) so we need to count our own cycles

        df['cycle number'] = df['charge'].ne(df['charge'].shift()).cumsum()
        df['cycle number'] = df['cycle number'].floordiv(2)

    #Adding attributes must be the last thing to do since it will not be copied when doing operations on the dataframe
    df.experiment_mode = mode

    return df
Beispiel #30
0
def check_df(df):
    """Check if the dataframe has:
    - cycle number (sometimes the Z1 counter f***s up and just says 0)
    - If it is GC data: then calculate capacity
    - removing data where the cell is resting
    
    If anything is wrong, it tries to fix it"""


    if df.experiment_mode == 1: #Then its GC
        if df['cycle number'].eq(0).all() or df['cycle number'].max() < 100: #If all cycle numbers are 0, then maybe Z1 counter was not iterated properly.
            LOG.info("We only found one cycle in '{}', and suspect this to be false. Checking now if there should be more cycles.".format(df.name))

            #We fix this by counting our own cycles.
            #Keeping track of signs of current (positive or negative) and cycle number
            prev_sign = True
            sign = True 
            cycle_number = 1
            new_cycle_indexes = []

            for i,current in df['<I>/mA'].items():

                if current > 0:
                    sign = True
                    df['charge'].at[i] = True
                elif current < 0:
                    sign = False
                    df['charge'].at[i] = False
                

                if prev_sign is False and sign is True:
                    #Changing from a discharge to a charge means new cycle
                    prev_sign = True
                    cycle_number += 1
                    #df['cycle number'].at[i-1] = cycle_number
                    new_cycle_indexes.append(i-1)
                    
                elif prev_sign is True and sign is False:
                    #Changing from a charge to a discharge
                    prev_sign = False
                    new_cycle_indexes.append(i-1)

                # In place editing of cycle number
                df['cycle number'].at[i] = cycle_number

            #Remove rows where a new experiment start (BatSmall has f****d datalogging here, where the current and voltage is the same as the prev step, but the capacity restarts)
            #df.drop(new_cycle_indexes, axis = 0, inplace = True)

            if cycle_number > 1:
                LOG.info("Found {} cycles in {}".format(cycle_number, df.name))
            else:
                LOG.info("There seems to only be one cycle. Sorry for wasting your time.")

        else: #charge bool then isnt fixed
            for i,current in df['<I>/mA'].items():

                if current > 0:
                    sign = True
                    df['charge'].at[i] = True
                elif current < 0:
                    sign = False
                    df['charge'].at[i] = False