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
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
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
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")
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:]
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]")
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]")
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]")
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]")
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
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
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()
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()
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)
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
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
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}" )
def get_data(self): # Read input file self.df = readers.read(self.fn) LOG.debug("Data has been read successfully")