Beispiel #1
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 #2
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 #3
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 #4
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 #5
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 #6
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 #7
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 #8
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 #9
0
    def edic_dQdV(self):
        import numpy as np
        import pandas as pd
        """Takes self.df and returns self.dQdVdata in the format:
        self.dQdVdata = [cycle1, cycle2, cycle3, ... , cycleN]
        cyclex = (chg, dchg)
        chg = np.array([[v1, v2, v3, ..., vn],[dqdv1, dqdv2, dqdv3, ..., dqdvn]]) where dqdv is dQ/dV"""
        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.dQdVdata = []

            #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)

                #The inserted Active material mass might differ from the one the software calculated. Thus we make our own capacity calculations.
                if self.am_mass:
                    from scipy import integrate
                    #Integrate current over time, returns mAh, divide by active mass to get gravimetric.

                    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 Exception as e:
                        LOG.debug(
                            f"something went wrong with the scipy.integrate.cumtrapz in cell.py under edit_GC: {e}"
                        )
                        if not dchgdat.empty:
                            dchgdat.loc[:, 'capacity/mAhg'] = 0
                        else:
                            dchgdat['capacity/mAhg'] = []

                def binit(x, y, Nbins=120):
                    # extract 120 elements evenly spaced in the data
                    Nbins = 120
                    binsize = int(len(x) / Nbins)
                    if binsize == 0:  # modulo by zero not allowed so filtering that here
                        return [0], [0]

                    i = 0
                    xar = []
                    yar = []
                    for i, (x, y) in enumerate(zip(x, y)):
                        if i % binsize == 0:
                            xar.append(x)
                            yar.append(y)
                    return xar, yar

                def moving_average(x, y, w=9):
                    if len(x) < w + 1 or len(y) < w + 1:
                        return [0], [0]

                    from scipy.signal import savgol_filter
                    # Savitzky-Golay filter
                    #y = savgol_filter(y, w, 3)
                    #x = savgol_filter(x, w, 3)
                    #Simple Moving average
                    x = np.convolve(x, np.ones(w), 'valid') / w
                    y = np.convolve(y, np.ones(w), 'valid') / w
                    return x, y

                def get_dqdv(x, y):
                    x, y = binit(x, y)
                    x, y = moving_average(x, y)

                    if len(x) < 2 or len(y) < 2:
                        return [0], [0]

                    dqdv = np.log(np.diff(x) / np.diff(y))
                    v = y[:-1]
                    return dqdv, v

                chg_dqdv, chg_v = get_dqdv(chgdat['capacity/mAhg'],
                                           chgdat['Ewe/V'])
                dchg_dqdv, dchg_v = get_dqdv(dchgdat['capacity/mAhg'],
                                             dchgdat['Ewe/V'])

                cycle = (np.array([chg_v,
                                   chg_dqdv]), np.array([dchg_v, dchg_dqdv]))
                self.dQdVdata.append(cycle)