diff --git a/beamtime/electrochemistry/__init__.py b/beamtime/electrochemistry/__init__.py index e0c4c87..0270f1d 100644 --- a/beamtime/electrochemistry/__init__.py +++ b/beamtime/electrochemistry/__init__.py @@ -1 +1 @@ -from . import io, plot +from . import io, plot, unit_tables diff --git a/beamtime/electrochemistry/io.py b/beamtime/electrochemistry/io.py index 23679ea..510be04 100644 --- a/beamtime/electrochemistry/io.py +++ b/beamtime/electrochemistry/io.py @@ -1,10 +1,27 @@ import pandas as pd import numpy as np import matplotlib.pyplot as plt +import os -def read_battsmall(path): - ''' Reads BATTSMALL-data into a DataFrame. +def read_data(path, kind, options=None): + + if kind == 'neware': + df = read_neware(path) + cycles = process_neware_data(df, options=options) + + elif kind == 'batsmall': + df = read_batsmall(path) + cycles = process_batsmall_data(df=df, options=options) + + elif kind == 'biologic': + df = read_biologic(path) + cycles = process_biologic_data(df=df, options=options) + + return cycles + +def read_batsmall(path): + ''' Reads BATSMALL-data into a DataFrame. Input: path (required): string with path to datafile @@ -19,67 +36,71 @@ def read_battsmall(path): -def unit_conversion(df, units): - C, m = units['C'].split('/') +def read_neware(path, summary=False): + ''' Reads electrochemistry data, currently only from the Neware battery cycler. Will convert to .csv if the filetype is .xlsx, + which is the file format the Neware provides for the backup data. In this case it matters if summary is False or not. If file + type is .csv, it will just open the datafile and it does not matter if summary is False or not.''' + from xlsx2csv import Xlsx2csv - # Get the units used in the data set - t_prev = df.columns[0].split()[-1].strip('[]') - U_prev = df.columns[1].split()[-1].strip('[]') - I_prev = df.columns[2].split()[-1].strip('[]') - C_prev, m_prev = df.columns[4].split()[-1].strip('[]').split('/') + # Convert from .xlsx to .csv to make readtime faster + if path.split('.')[-1] == 'xlsx': + csv_details = ''.join(path.split('.')[:-1]) + '_details.csv' + csv_summary = ''.join(path.split('.')[:-1]) + '_summary.csv' + if not os.path.isfile(csv_summary): + Xlsx2csv(path, outputencoding="utf-8").convert(csv_summary, sheetid=3) - # Define matrix for unit conversion for time - t_units_df = {'h': [1, 60, 3600, 3600000], 'min': [1/60, 1, 60, 60000], 's': [1/3600, 1/60, 1, 1000], 'ms': [1/3600000, 1/60000, 1/1000, 1]} - t_units_df = pd.DataFrame(t_units_df) - t_units_df.index = ['h', 'min', 's', 'ms'] + if not os.path.isfile(csv_details): + Xlsx2csv(path, outputencoding="utf-8").convert(csv_details, sheetid=4) - # Define matrix for unit conversion for current - I_units_df = {'A': [1, 1000, 1000000], 'mA': [1/1000, 1, 1000], 'uA': [1/1000000, 1/1000, 1]} - I_units_df = pd.DataFrame(I_units_df) - I_units_df.index = ['A', 'mA', 'uA'] - - # Define matrix for unit conversion for voltage - U_units_df = {'V': [1, 1000, 1000000], 'mV': [1/1000, 1, 1000], 'uV': [1/1000000, 1/1000, 1]} - U_units_df = pd.DataFrame(U_units_df) - U_units_df.index = ['V', 'mV', 'uV'] - - # Define matrix for unit conversion for capacity - C_units_df = {'Ah': [1, 1000, 1000000], 'mAh': [1/1000, 1, 1000], 'uAh': [1/1000000, 1/1000, 1]} - C_units_df = pd.DataFrame(C_units_df) - C_units_df.index = ['Ah', 'mAh', 'uAh'] - - # Define matrix for unit conversion for capacity - m_units_df = {'kg': [1, 1000, 1000000, 1000000000], 'g': [1/1000, 1, 1000, 1000000], 'mg': [1/1000000, 1/1000, 1, 1000], 'ug': [1/1000000000, 1/1000000, 1/1000, 1]} - m_units_df = pd.DataFrame(m_units_df) - m_units_df.index = ['kg', 'g', 'mg', 'ug'] - - - - - #print(df["TT [{}]".format(t_prev)]) - df["TT [{}]".format(t_prev)] = df["TT [{}]".format(t_prev)] * t_units_df[t_prev].loc[units['t']] - df["U [{}]".format(U_prev)] = df["U [{}]".format(U_prev)] * U_units_df[U_prev].loc[units['U']] - df["I [{}]".format(I_prev)] = df["I [{}]".format(I_prev)] * I_units_df[I_prev].loc[units['I']] - df["C [{}/{}]".format(C_prev, m_prev)] = df["C [{}/{}]".format(C_prev, m_prev)] * (C_units_df[C_prev].loc[units['C']] / m_units_df[m_prev].loc[units['m']]) - - df.columns = ['TT', 'U', 'I', 'Z1', 'C', 'Comment'] + if summary: + df = pd.read_csv(csv_summary) + else: + df = pd.read_csv(csv_details) + elif path.split('.')[-1] == 'csv': + df = pd.read_csv(path) return df -#def process_battsmall_data(df, t='ms', C='mAh/g', I='mA', U='V'): -def process_battsmall_data(df, units=None): - ''' Takes BATTSMALL-data in the form of a DataFrame and cleans the data up and converts units into desired units. + +def read_biologic(path): + ''' Reads Bio-Logic-data into a DataFrame. + + Input: + path (required): string with path to datafile + + Output: + df: pandas DataFrame containing the data as-is, but without additional NaN-columns.''' + + with open(path, 'r') as f: + lines = f.readlines() + + header_lines = int(lines[1].split()[-1]) - 1 + + + df = pd.read_csv(path, sep='\t', skiprows=header_lines) + df.dropna(inplace=True, axis=1) + + return df + + + + + + + +def process_batsmall_data(df, options=None): + ''' Takes BATSMALL-data in the form of a DataFrame and cleans the data up and converts units into desired units. Splits up into individual charge and discharge DataFrames per cycle, and outputs a list where each element is a tuple with the Chg and DChg-data. E.g. cycles[10][0] gives the charge data for the 11th cycle. For this to work, the cycling program must be set to use the counter. Input: - df (required): A pandas DataFrame containing BATTSMALL-data, as obtained from read_battsmall(). + df (required): A pandas DataFrame containing BATSMALL-data, as obtained from read_batsmall(). t (optional): Unit for time data. Defaults to ms. C (optional): Unit for specific capacity. Defaults to mAh/g. I (optional): Unit for current. Defaults mA. @@ -89,84 +110,426 @@ def process_battsmall_data(df, units=None): cycles: A list with ''' - required_units = ['t', 'I', 'U', 'C'] - default_units = {'t': 'h', 'I': 'mA', 'U': 'V', 'C': 'mAh/g'} + required_options = ['splice_cycles', 'molecular_weight', 'reverse_discharge', 'units'] + default_options = {'splice_cycles': None, 'molecular_weight': None, 'reverse_discharge': False, 'units': None} + + if not options: + options = default_options + else: + for option in required_options: + if option not in options.keys(): + options[option] = default_options[option] + + + # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. + new_units = set_units(units=options['units']) + old_units = get_old_units(df, kind='batsmall') + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='batsmall') + + options['units'] = new_units + + # Replace NaN with empty string in the Comment-column and then remove all steps where the program changes - this is due to inconsistent values for current + df[["comment"]] = df[["comment"]].fillna(value={'comment': ''}) + df = df[df["comment"].str.contains("program")==False] + + # Creates masks for charge and discharge curves + chg_mask = df['current'] >= 0 + dchg_mask = df['current'] < 0 + + # Initiate cycles list + cycles = [] + + # Loop through all the cycling steps, change the current and capacities in the + for i in range(df["count"].max()): + + sub_df = df.loc[df['count'] == i].copy() + + sub_df.loc[dchg_mask, 'current'] *= -1 + sub_df.loc[dchg_mask, 'specific_capacity'] *= -1 + + chg_df = sub_df.loc[chg_mask] + dchg_df = sub_df.loc[dchg_mask] + + # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) + if chg_df.empty and dchg_df.empty: + continue + + if options['reverse_discharge']: + max_capacity = dchg_df['capacity'].max() + dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) + + if 'specific_capacity' in df.columns: + max_capacity = dchg_df['specific_capacity'].max() + dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) + + if 'ions' in df.columns: + max_capacity = dchg_df['ions'].max() + dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) + + cycles.append((chg_df, dchg_df)) + + + + + return cycles + + +def process_neware_data(df, options=None): + + """ Takes data from NEWARE in a DataFrame as read by read_neware() and converts units, adds columns and splits into cycles. + + Input: + df: pandas DataFrame containing NEWARE data as read by read_neware() + units: dictionary containing the desired units. keywords: capacity, current, voltage, mass, energy, time + splice_cycles: tuple containing index of cycles that should be spliced. Specifically designed to add two charge steps during the formation cycle with two different max voltages + active_materiale_weight: weight of the active material (in mg) used in the cell. + molecular_weight: the molar mass (in g mol^-1) of the active material, to calculate the number of ions extracted. Assumes one electron per Li+/Na+-ion """ + + required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles'] + default_options = {'units': None, 'active_material_weight': None, 'molecular_weight': None, 'reverse_discharge': False, 'splice_cycles': None} + + if not options: + options = default_options + else: + for option in required_options: + if option not in options.keys(): + options[option] = default_options[option] + + + # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. + new_units = set_units(units=options['units']) + old_units = get_old_units(df=df, kind='neware') + + df = add_columns(df=df, active_material_weight=options['active_material_weight'], molecular_weight=options['molecular_weight'], old_units=old_units, kind='neware') + + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='neware') + + options['units'] = new_units + + + # Creates masks for charge and discharge curves + chg_mask = df['status'] == 'CC Chg' + dchg_mask = df['status'] == 'CC DChg' + + # Initiate cycles list + cycles = [] + + # Loop through all the cycling steps, change the current and capacities in the + for i in range(df["cycle"].max()): + + sub_df = df.loc[df['cycle'] == i].copy() + + #sub_df.loc[dchg_mask, 'current'] *= -1 + #sub_df.loc[dchg_mask, 'capacity'] *= -1 + + chg_df = sub_df.loc[chg_mask] + dchg_df = sub_df.loc[dchg_mask] + + # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) + if chg_df.empty and dchg_df.empty: + continue + + if options['reverse_discharge']: + max_capacity = dchg_df['capacity'].max() + dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) + + if 'specific_capacity' in df.columns: + max_capacity = dchg_df['specific_capacity'].max() + dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) + + if 'ions' in df.columns: + max_capacity = dchg_df['ions'].max() + dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) + + cycles.append((chg_df, dchg_df)) + + + + return cycles + + +def process_biologic_data(df, options=None): + + required_options = ['units', 'active_material_weight', 'molecular_weight', 'reverse_discharge', 'splice_cycles'] + default_options = {'units': None, 'active_material_weight': None, 'molecular_weight': None, 'reverse_discharge': False, 'splice_cycles': None} + + if not options: + options = default_options + else: + for option in required_options: + if option not in options.keys(): + options[option] = default_options[option] + + # Pick out necessary columns + df = df[['Ns changes', 'Ns', 'time/s', 'Ewe/V', 'Energy charge/W.h', 'Energy discharge/W.h', '/mA', 'Capacity/mA.h', 'cycle number']].copy() + + # Complete set of new units and get the units used in the dataset, and convert values in the DataFrame from old to new. + new_units = set_units(units=options['units']) + old_units = get_old_units(df=df, kind='biologic') + + df = add_columns(df=df, active_material_weight=options['active_material_weight'], molecular_weight=options['molecular_weight'], old_units=old_units, kind='biologic') + + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='biologic') + + options['units'] = new_units + + + # Creates masks for charge and discharge curves + chg_mask = (df['status'] == 1) & (df['status_change'] != 1) + dchg_mask = (df['status'] == 2) & (df['status_change'] != 1) + + + + # Initiate cycles list + cycles = [] + + # Loop through all the cycling steps, change the current and capacities in the + for i in range(int(df["cycle"].max())): + + sub_df = df.loc[df['cycle'] == i].copy() + + #sub_df.loc[dchg_mask, 'current'] *= -1 + #sub_df.loc[dchg_mask, 'capacity'] *= -1 + + chg_df = sub_df.loc[chg_mask] + dchg_df = sub_df.loc[dchg_mask] + + # Continue to next iteration if the charge and discharge DataFrames are empty (i.e. no current) + if chg_df.empty and dchg_df.empty: + continue + + if options['reverse_discharge']: + max_capacity = dchg_df['capacity'].max() + dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) + + if 'specific_capacity' in df.columns: + max_capacity = dchg_df['specific_capacity'].max() + dchg_df['specific_capacity'] = np.abs(dchg_df['specific_capacity'] - max_capacity) + + if 'ions' in df.columns: + max_capacity = dchg_df['ions'].max() + dchg_df['ions'] = np.abs(dchg_df['ions'] - max_capacity) + + cycles.append((chg_df, dchg_df)) + + + + return cycles + + +def add_columns(df, active_material_weight, molecular_weight, old_units, kind): + + if kind == 'neware': + if active_material_weight: + df["SpecificCapacity({}/mg)".format(old_units["capacity"])] = df["Capacity({})".format(old_units['capacity'])] / (active_material_weight) + + if molecular_weight: + faradays_constant = 96485.3365 # [F] = C mol^-1 = As mol^-1 + seconds_per_hour = 3600 # s h^-1 + f = faradays_constant / seconds_per_hour * 1000.0 # [f] = mAh mol^-1 + + df["IonsExtracted"] = (df["SpecificCapacity({}/mg)".format(old_units['capacity'])]*molecular_weight)*1000/f + + + if kind == 'biologic': + if active_material_weight: + + capacity = old_units['capacity'].split('h')[0] + '.h' + + + df["SpecificCapacity({}/mg)".format(old_units["capacity"])] = df["Capacity/{}".format(capacity)] / (active_material_weight) + + if molecular_weight: + faradays_constant = 96485.3365 # [F] = C mol^-1 = As mol^-1 + seconds_per_hour = 3600 # s h^-1 + f = faradays_constant / seconds_per_hour * 1000.0 # [f] = mAh mol^-1 + + df["IonsExtracted"] = (df["SpecificCapacity({}/mg)".format(old_units['capacity'])]*molecular_weight)*1000/f + + return df + + +def unit_conversion(df, new_units, old_units, kind): + from . import unit_tables + + if kind == 'batsmall': + + df["TT [{}]".format(old_units["time"])] = df["TT [{}]".format(old_units["time"])] * unit_tables.time()[old_units["time"]].loc[new_units['time']] + df["U [{}]".format(old_units["voltage"])] = df["U [{}]".format(old_units["voltage"])] * unit_tables.voltage()[old_units["voltage"]].loc[new_units['voltage']] + df["I [{}]".format(old_units["current"])] = df["I [{}]".format(old_units["current"])] * unit_tables.current()[old_units["current"]].loc[new_units['current']] + df["C [{}/{}]".format(old_units["capacity"], old_units["mass"])] = df["C [{}/{}]".format(old_units["capacity"], old_units["mass"])] * (unit_tables.capacity()[old_units["capacity"]].loc[new_units["capacity"]] / unit_tables.mass()[old_units["mass"]].loc[new_units["mass"]]) + + df.columns = ['time', 'voltage', 'current', 'count', 'specific_capacity', 'comment'] + + + if kind == 'neware': + df['Current({})'.format(old_units['current'])] = df['Current({})'.format(old_units['current'])] * unit_tables.current()[old_units['current']].loc[new_units['current']] + df['Voltage({})'.format(old_units['voltage'])] = df['Voltage({})'.format(old_units['voltage'])] * unit_tables.voltage()[old_units['voltage']].loc[new_units['voltage']] + df['Capacity({})'.format(old_units['capacity'])] = df['Capacity({})'.format(old_units['capacity'])] * unit_tables.capacity()[old_units['capacity']].loc[new_units['capacity']] + df['Energy({})'.format(old_units['energy'])] = df['Energy({})'.format(old_units['energy'])] * unit_tables.energy()[old_units['energy']].loc[new_units['energy']] + df['CycleTime({})'.format(new_units['time'])] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=new_units['time']), axis=1) + df['RunTime({})'.format(new_units['time'])] = df.apply(lambda row : convert_datetime_string(row['Real Time(h:min:s.ms)'], reference=df['Real Time(h:min:s.ms)'].iloc[0], unit=new_units['time']), axis=1) + columns = ['status', 'jump', 'cycle', 'steps', 'current', 'voltage', 'capacity', 'energy'] + + if 'SpecificCapacity({}/mg)'.format(old_units['capacity']) in df.columns: + df['SpecificCapacity({}/mg)'.format(old_units['capacity'])] = df['SpecificCapacity({}/mg)'.format(old_units['capacity'])] * unit_tables.capacity()[old_units['capacity']].loc[new_units['capacity']] / unit_tables.mass()['mg'].loc[new_units["mass"]] + columns.append('specific_capacity') + + if 'IonsExtracted' in df.columns: + columns.append('ions') + + + + columns.append('cycle_time') + columns.append('time') + + + df.drop(['Record number', 'Relative Time(h:min:s.ms)', 'Real Time(h:min:s.ms)'], axis=1, inplace=True) + + df.columns = columns + + if kind == 'biologic': + df['time/{}'.format(old_units['time'])] = df["time/{}".format(old_units["time"])] * unit_tables.time()[old_units["time"]].loc[new_units['time']] + df["Ewe/{}".format(old_units["voltage"])] = df["Ewe/{}".format(old_units["voltage"])] * unit_tables.voltage()[old_units["voltage"]].loc[new_units['voltage']] + df["/{}".format(old_units["current"])] = df["/{}".format(old_units["current"])] * unit_tables.current()[old_units["current"]].loc[new_units['current']] + + capacity = old_units['capacity'].split('h')[0] + '.h' + df["Capacity/{}".format(capacity)] = df["Capacity/{}".format(capacity)] * (unit_tables.capacity()[old_units["capacity"]].loc[new_units["capacity"]]) + + columns = ['status_change', 'status', 'time', 'voltage', 'energy_charge', 'energy_discharge', 'current', 'capacity', 'cycle'] + + if 'SpecificCapacity({}/mg)'.format(old_units['capacity']) in df.columns: + df['SpecificCapacity({}/mg)'.format(old_units['capacity'])] = df['SpecificCapacity({}/mg)'.format(old_units['capacity'])] * unit_tables.capacity()[old_units['capacity']].loc[new_units['capacity']] / unit_tables.mass()['mg'].loc[new_units["mass"]] + columns.append('specific_capacity') + + if 'IonsExtracted' in df.columns: + columns.append('ions') + + df.columns = columns + + return df + + +def set_units(units=None): + + # Complete the list of units - if not all are passed, then default value will be used + required_units = ['time', 'current', 'voltage', 'capacity', 'mass', 'energy', 'specific_capacity'] + default_units = {'time': 'h', 'current': 'mA', 'voltage': 'V', 'capacity': 'mAh', 'mass': 'g', 'energy': 'mWh', 'specific_capacity': None} if not units: units = default_units if units: for unit in required_units: - if unit not in units.values(): + if unit not in units.keys(): units[unit] = default_units[unit] - - # Convert all units to the desired units. - df = unit_conversion(df=df, units=units) - - # Replace NaN with empty string in the Comment-column and then remove all steps where the program changes - this is due to inconsistent values for current and - df[["Comment"]] = df[["Comment"]].fillna(value={'Comment': ''}) - df = df[df["Comment"].str.contains("program")==False] - - # Creates masks for - chg_mask = df['I'] >= 0 - dchg_mask = df['I'] < 0 - - # Initiate cycles list - cycles = [] - - # Loop through all the cycling steps, change the current and capacities in the - for i in range(df["Z1"].max()): - sub_df = df.loc[df['Z1'] == i].copy() - sub_df.loc[dchg_mask, 'I'] *= -1 - sub_df.loc[dchg_mask, 'C'] *= -1 - - chg_df = sub_df.loc[chg_mask] - dchg_df = sub_df.loc[dchg_mask] + units['specific_capacity'] = r'{} {}'.format(units['capacity'], units['mass']) + '$^{-1}$' - cycles.append((chg_df, dchg_df)) - - - return cycles + return units -def plot_gc(cycles, which_cycles='all', chg=True, dchg=True, colours=None, x='C', y='U'): - - fig, ax = prepare_gc_plot() - - - if which_cycles == 'all': - which_cycles = [i for i, c in enumerate(cycles)] - - if not colours: - chg_colour = (40/255, 70/255, 75/255) # Dark Slate Gray #28464B - dchg_colour = (239/255, 160/255, 11/255) # Marigold #EFA00B - +def get_old_units(df, kind): + if kind=='batsmall': + time = df.columns[0].split()[-1].strip('[]') + voltage = df.columns[1].split()[-1].strip('[]') + current = df.columns[2].split()[-1].strip('[]') + capacity, mass = df.columns[4].split()[-1].strip('[]').split('/') + old_units = {'time': time, 'current': current, 'voltage': voltage, 'capacity': capacity, 'mass': mass} - for i, cycle in cycles: - if i in which_cycles: - if chg: - cycle[0].plot(ax=ax) + if kind=='neware': + + for column in df.columns: + if 'Voltage' in column: + voltage = column.split('(')[-1].strip(')') + elif 'Current' in column: + current = column.split('(')[-1].strip(')') + elif 'Capacity' in column: + capacity = column.split('(')[-1].strip(')') + elif 'Energy' in column: + energy = column.split('(')[-1].strip(')') + + old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy} + if kind=='biologic': + for column in df.columns: + if 'time' in column: + time = column.split('/')[-1] + elif 'Ewe' in column: + voltage = column.split('/')[-1] + elif 'Capacity' in column: + capacity = column.split('/')[-1].replace('.', '') + elif 'Energy' in column: + energy = column.split('/')[-1].replace('.', '') + elif '' in column: + current = column.split('/')[-1] + old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy, 'time': time} + return old_units + +def convert_time_string(time_string, unit='ms'): + ''' Convert time string from Neware-data with the format hh:mm:ss.xx to any given unit''' + + h, m, s = time_string.split(':') + ms = float(s)*1000 + int(m)*1000*60 + int(h)*1000*60*60 + + factors = {'ms': 1, 's': 1/1000, 'min': 1/(1000*60), 'h': 1/(1000*60*60)} + + t = ms*factors[unit] + + return t +def convert_datetime_string(datetime_string, reference, unit='s'): + ''' Convert time string from Neware-data with the format yyy-mm-dd hh:mm:ss to any given unit''' -def prepare_gc_plot(figsize=(14,7), dpi=None): + from datetime import datetime + + # Parse the + current_date, current_time = datetime_string.split() + current_year, current_month, current_day = current_date.split('-') + current_hour, current_minute, current_second = current_time.split(':') + current_date = datetime(int(current_year), int(current_month), int(current_day), int(current_hour), int(current_minute), int(current_second)) + + reference_date, reference_time = reference.split() + reference_year, reference_month, reference_day = reference_date.split('-') + reference_hour, reference_minute, reference_second = reference_time.split(':') + reference_date = datetime(int(reference_year), int(reference_month), int(reference_day), int(reference_hour), int(reference_minute), int(reference_second)) + + days = current_date - reference_date + + + s = days.days*24*60*60 + days.seconds + + factors = {'ms': 1000, 's': 1, 'min': 1/(60), 'h': 1/(60*60)} + + time = s * factors[unit] + + return time + +def splice_cycles(first, second): + + first_chg = first[0] + first_dchg = first[1] + first + + second_chg = second[0] + second_dchg = second[1] + + chg_df = first[0].append(second[0]) + + return True - fig, ax = plt.subplots(figsize=figsize, dpi=dpi) - - return fig, ax diff --git a/beamtime/electrochemistry/plot.py b/beamtime/electrochemistry/plot.py index d820b59..13bb1e1 100644 --- a/beamtime/electrochemistry/plot.py +++ b/beamtime/electrochemistry/plot.py @@ -1 +1,386 @@ import matplotlib.pyplot as plt +from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator) + +import pandas as pd +import numpy as np +import math + +import beamtime.electrochemistry as ec + + +def plot_gc(path, kind, options=None): + + # Prepare plot, and read and process data + fig, ax = prepare_gc_plot(options=options) + cycles = ec.io.read_data(path=path, kind=kind, options=options) + + + # Update options + required_options = ['x_vals', 'y_vals', 'which_cycles', 'chg', 'dchg', 'colours', 'differentiate_charge_discharge', 'gradient'] + default_options = {'x_vals': 'capacity', 'y_vals': 'voltage', 'which_cycles': 'all', 'chg': True, 'dchg': True, 'colours': None, 'differentiate_charge_discharge': True, 'gradient': False} + + options = update_options(options=options, required_options=required_options, default_options=default_options) + + # Update list of cycles to correct indices + update_cycles_list(cycles=cycles, options=options) + + colours = generate_colours(cycles=cycles, options=options) + + + for i, cycle in enumerate(cycles): + if i in options['which_cycles']: + if options['chg']: + cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0]) + + if options['dchg']: + cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1]) + + + + fig, ax = prettify_gc_plot(fig=fig, ax=ax, options=options) + + return cycles, fig, ax + + +def update_options(options, required_options, default_options): + + if not options: + options = default_options + + else: + for option in required_options: + if option not in options.keys(): + options[option] = default_options[option] + + return options + +def update_cycles_list(cycles, options): + + if not options: + options['which_cycles'] + + if options['which_cycles'] == 'all': + options['which_cycles'] = [i for i in range(len(cycles))] + + + elif type(options['which_cycles']) == list: + options['which_cycles'] = [i-1 for i in options['which_cycles']] + + + # Tuple is used to define an interval - as elements tuples can't be assigned, I convert it to a list here. + elif type(options['which_cycles']) == tuple: + which_cycles = list(options['which_cycles']) + + if which_cycles[0] <= 0: + which_cycles[0] = 1 + + elif which_cycles[1] < 0: + which_cycles[1] = len(cycles) + + + options['which_cycles'] = [i-1 for i in range(which_cycles[0], which_cycles[1]+1)] + + + return options + + +def prepare_gc_plot(options=None): + + + # First take care of the options for plotting - set any values not specified to the default values + required_options = ['columns', 'width', 'height', 'format', 'dpi', 'facecolor'] + default_options = {'columns': 1, 'width': 14, 'format': 'golden_ratio', 'dpi': None, 'facecolor': 'w'} + + # If none are set at all, just pass the default_options + if not options: + options = default_options + options['height'] = options['width'] * (math.sqrt(5) - 1) / 2 + options['figsize'] = (options['width'], options['height']) + + + # If options is passed, go through to fill out the rest. + else: + # Start by setting the width: + if 'width' not in options.keys(): + options['width'] = default_options['width'] + + # Then set height - check options for format. If not given, set the height to the width scaled by the golden ratio - if the format is square, set the same. This should possibly allow for the tweaking of custom ratios later. + if 'height' not in options.keys(): + if 'format' not in options.keys(): + options['height'] = options['width'] * (math.sqrt(5) - 1) / 2 + elif options['format'] == 'square': + options['height'] = options['width'] + + options['figsize'] = (options['width'], options['height']) + + # After height and width are set, go through the rest of the options to make sure that all the required options are filled + for option in required_options: + if option not in options.keys(): + options[option] = default_options[option] + + fig, ax = plt.subplots(figsize=(options['figsize']), dpi=options['dpi'], facecolor=options['facecolor']) + + linewidth = 1*options['columns'] + axeswidth = 3*options['columns'] + + plt.rc('lines', linewidth=linewidth) + plt.rc('axes', linewidth=axeswidth) + + return fig, ax + + +def prettify_gc_plot(fig, ax, options=None): + + + ################################################################## + ######################### UPDATE OPTIONS ######################### + ################################################################## + + # Define the required options + required_options = [ + 'columns', + 'xticks', 'yticks', + 'show_major_ticks', 'show_minor_ticks', + 'xlim', 'ylim', + 'hide_x_axis', 'hide_y_axis', + 'positions', + 'x_vals', 'y_vals', + 'xlabel', 'ylabel', + 'units', 'sizes', + 'title' + ] + + + # Define the default options + default_options = { + 'columns': 1, + 'xticks': None, 'yticks': None, + 'show_major_ticks': [True, True, True, True], 'show_minor_ticks': [True, True, True, True], + 'xlim': None,'ylim': None, + 'hide_x_axis': False, 'hide_y_axis': False, + 'positions': {'xaxis': 'bottom', 'yaxis': 'left'}, + 'x_vals': 'specific_capacity', 'y_vals': 'voltage', + 'xlabel': None, 'ylabel': None, + 'units': None, + 'sizes': None, + 'title': None + } + + update_options(options, required_options, default_options) + + + ################################################################## + ########################## DEFINE SIZES ########################## + ################################################################## + + # Define the required sizes + required_sizes = [ + 'labels', + 'legend', + 'title', + 'line', 'axes', + 'tick_labels', + 'major_ticks', 'minor_ticks'] + + + + # Define default sizes + default_sizes = { + 'labels': 30*options['columns'], + 'legend': 30*options['columns'], + 'title': 30*options['columns'], + 'line': 3*options['columns'], + 'axes': 3*options['columns'], + 'tick_labels': 30*options['columns'], + 'major_ticks': 20*options['columns'], + 'minor_ticks': 10*options['columns'] + } + + # Initialise dictionary if it doesn't exist + if not options['sizes']: + options['sizes'] = {} + + + # Update dictionary with default values where none is supplied + for size in required_sizes: + if size not in options['sizes']: + options['sizes'][size] = default_sizes[size] + + + ################################################################## + ########################## AXIS LABELS ########################### + ################################################################## + + + if not options['xlabel']: + options['xlabel'] = prettify_labels(options['x_vals']) + ' [{}]'.format(options['units'][options['x_vals']]) + + else: + options['xlabel'] = options['xlabel'] + ' [{}]'.format(options['units'][options['x_vals']]) + + + if not options['ylabel']: + options['ylabel'] = prettify_labels(options['y_vals']) + ' [{}]'.format(options['units'][options['y_vals']]) + + else: + options['ylabel'] = options['ylabel'] + ' [{}]'.format(options['units'][options['y_vals']]) + + ax.set_xlabel(options['xlabel'], size=options['sizes']['labels']) + ax.set_ylabel(options['ylabel'], size=options['sizes']['labels']) + + ################################################################## + ###################### TICK MARKS & LABELS ####################### + ################################################################## + + ax.tick_params(direction='in', which='major', bottom=options['show_major_ticks'][0], left=options['show_major_ticks'][1], top=options['show_major_ticks'][2], right=options['show_major_ticks'][0], length=options['sizes']['major_ticks'], width=options['sizes']['axes']) + ax.tick_params(direction='in', which='minor', bottom=options['show_minor_ticks'][0], left=options['show_minor_ticks'][1], top=options['show_minor_ticks'][2], right=options['show_minor_ticks'][0], length=options['sizes']['minor_ticks'], width=options['sizes']['axes']) + + + + # DEFINE AND SET TICK DISTANCES + + from . import unit_tables + + # Define default ticks and scale to desired units + default_ticks = { + 'specific_capacity': [100 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']] / unit_tables.mass()['g'].loc[options['units']['mass']]), 50 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']] / unit_tables.mass()['g'].loc[options['units']['mass']])], + 'capacity': [0.1 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']]), 0.05 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']])], + 'voltage': [0.5 * (unit_tables.voltage()['V'].loc[options['units']['voltage']]), 0.25 * (unit_tables.voltage()['V'].loc[options['units']['voltage']])], + 'time': [10 * (unit_tables.time()['h'].loc[options['units']['time']]), 5 * (unit_tables.time()['h'].loc[options['units']['time']])] + } + + + if options['positions']['yaxis'] == 'right': + ax.yaxis.set_label_position("right") + ax.yaxis.tick_right() + + + # Set default tick distances for x-axis if not specified + if not options['xticks']: + + major_xtick = default_ticks[options['x_vals']][0] + minor_xtick = default_ticks[options['x_vals']][1] + + # Otherwise apply user input + else: + major_xtick = options['xticks'][0] + minor_xtick = options['xticks'][1] + + + # Set default tick distances for x-axis if not specified + if not options['yticks']: + + major_ytick = default_ticks[options['y_vals']][0] + minor_ytick = default_ticks[options['y_vals']][1] + + # Otherwise apply user input + else: + major_xtick = options['yticks'][0] + minor_xtick = options['yticks'][1] + + + # Apply values + ax.xaxis.set_major_locator(MultipleLocator(major_xtick)) + ax.xaxis.set_minor_locator(MultipleLocator(minor_xtick)) + + ax.yaxis.set_major_locator(MultipleLocator(major_ytick)) + ax.yaxis.set_minor_locator(MultipleLocator(minor_ytick)) + + + + + # SET FONTSIZE OF TICK LABELS + + plt.xticks(fontsize=options['sizes']['tick_labels']) + plt.yticks(fontsize=options['sizes']['tick_labels']) + + ################################################################## + ########################## AXES LIMITS ########################### + ################################################################## + + if options['xlim']: + plt.xlim(options['xlim']) + + if options['ylim']: + plt.ylim(options['ylim']) + + ################################################################## + ############################# TITLE ############################## + ################################################################## + + if options['title']: + ax.set_title(options['title'], size=options['sizes']['title']) + + ################################################################## + ############################# LEGEND ############################# + ################################################################## + + ax.get_legend().remove() + + return fig, ax + + +def prettify_labels(label): + + labels_dict = { + 'capacity': 'Capacity', + 'specific_capacity': 'Specific capacity', + 'voltage': 'Voltage', + 'current': 'Current', + 'energy': 'Energy', + 'time': 'Time' + } + + return labels_dict[label] + + + + + + + + +def generate_colours(cycles, options): + + # Assign colours from the options dictionary if it is defined, otherwise use standard colours. + if options['colours']: + charge_colour = options['colours'][0] + discharge_colour = options['colours'][1] + + else: + charge_colour = (40/255, 70/255, 75/255) # Dark Slate Gray #28464B, coolors.co + discharge_colour = (239/255, 160/255, 11/255) # Marigold #EFA00B, coolors.co + + if not options['differentiate_charge_discharge']: + discharge_colour = charge_colour + + + + # If gradient is enabled, find start and end points for each colour + if options['gradient']: + + add_charge = min([(1-x)*0.75 for x in charge_colour]) + add_discharge = min([(1-x)*0.75 for x in discharge_colour]) + + charge_colour_start = charge_colour + charge_colour_end = [x+add_charge for x in charge_colour] + + discharge_colour_start = discharge_colour + discharge_colour_end = [x+add_discharge for x in discharge_colour] + + + + # Generate lists of colours + colours = [] + + for cycle_number in range(0, len(cycles)): + if options['gradient']: + weight_start = (len(cycles) - cycle_number)/len(cycles) + weight_end = cycle_number/len(cycles) + + charge_colour = [weight_start*start_colour + weight_end*end_colour for start_colour, end_colour in zip(charge_colour_start, charge_colour_end)] + discharge_colour = [weight_start*start_colour + weight_end*end_colour for start_colour, end_colour in zip(discharge_colour_start, discharge_colour_end)] + + colours.append([charge_colour, discharge_colour]) + + return colours diff --git a/beamtime/electrochemistry/unit_tables.py b/beamtime/electrochemistry/unit_tables.py new file mode 100644 index 0000000..c839b9b --- /dev/null +++ b/beamtime/electrochemistry/unit_tables.py @@ -0,0 +1,53 @@ +import pandas as pd + +def time(): + # Define matrix for unit conversion for time + time = {'h': [1, 60, 3600, 3600000], 'min': [1/60, 1, 60, 60000], 's': [1/3600, 1/60, 1, 1000], 'ms': [1/3600000, 1/60000, 1/1000, 1]} + time = pd.DataFrame(time) + time.index = ['h', 'min', 's', 'ms'] + + return time + +def current(): + # Define matrix for unit conversion for current + current = {'A': [1, 1000, 1000000], 'mA': [1/1000, 1, 1000], 'uA': [1/1000000, 1/1000, 1]} + current = pd.DataFrame(current) + current.index = ['A', 'mA', 'uA'] + + return current + +def voltage(): + # Define matrix for unit conversion for voltage + voltage = {'V': [1, 1000, 1000000], 'mV': [1/1000, 1, 1000], 'uV': [1/1000000, 1/1000, 1]} + voltage = pd.DataFrame(voltage) + voltage.index = ['V', 'mV', 'uV'] + + return voltage + +def capacity(): + # Define matrix for unit conversion for capacity + capacity = {'Ah': [1, 1000, 1000000], 'mAh': [1/1000, 1, 1000], 'uAh': [1/1000000, 1/1000, 1]} + capacity = pd.DataFrame(capacity) + capacity.index = ['Ah', 'mAh', 'uAh'] + + return capacity + +def mass(): + # Define matrix for unit conversion for capacity + mass = {'kg': [1, 1000, 1000000, 1000000000], 'g': [1/1000, 1, 1000, 1000000], 'mg': [1/1000000, 1/1000, 1, 1000], 'ug': [1/1000000000, 1/1000000, 1/1000, 1]} + mass = pd.DataFrame(mass) + mass.index = ['kg', 'g', 'mg', 'ug'] + + return mass + + +def energy(): + + energy = {'kWh': [1, 1000, 1000000], 'Wh': [1/1000, 1, 1000], 'mWh': [1/100000, 1/1000, 1]} + energy = pd.DataFrame(energy) + energy.index = ['kWh', 'Wh', 'mWh'] + + return energy + + + diff --git a/beamtime/requirements.txt b/beamtime/requirements.txt new file mode 100644 index 0000000..3f449e8 --- /dev/null +++ b/beamtime/requirements.txt @@ -0,0 +1,107 @@ +# This file may be used to create an environment using: +# $ conda create --name --file +# platform: win-64 +backcall=0.2.0=py_0 +beamtime=0.1=pypi_0 +blas=1.0=mkl +bottleneck=1.3.2=py39h7cc1a96_1 +ca-certificates=2021.10.8=h5b45459_0 +cached-property=1.5.2=hd8ed1ab_1 +cached_property=1.5.2=pyha770c72_1 +certifi=2021.10.8=py39hcbf5309_0 +colorama=0.4.4=py_0 +cycler=0.10.0=py_2 +debugpy=1.4.1=py39hd77b12b_0 +decorator=4.4.2=py_0 +fabio=0.12.0=py39h5d4886f_0 +freetype=2.10.4=h546665d_1 +glymur=0.9.4=pyhd8ed1ab_0 +h5py=3.2.1=nompi_py39hf27771d_100 +hdf5=1.10.6=nompi_h5268f04_1114 +hdf5plugin=3.1.1=py39h71586dd_0 +icc_rt=2019.0.0=h0cc432a_1 +icu=68.1=h0e60522_0 +intel-openmp=2021.3.0=haa95532_3372 +ipykernel=6.4.1=py39haa95532_1 +ipython=7.27.0=py39hd4e2768_0 +ipython_genutils=0.2.0=pyhd3eb1b0_1 +jbig=2.1=h8d14728_2003 +jedi=0.18.0=py39haa95532_1 +jpeg=9d=h8ffe710_0 +jupyter_client=6.1.7=py_0 +jupyter_core=4.8.1=py39haa95532_0 +kiwisolver=1.3.2=py39h2e07f2f_0 +krb5=1.19.2=hbae68bd_2 +lcms2=2.12=h2a16943_0 +lerc=3.0=h0e60522_0 +libclang=11.1.0=default_h5c34c98_1 +libcurl=7.79.1=h789b8ee_1 +libdeflate=1.8=h8ffe710_0 +libiconv=1.16=he774522_0 +libpng=1.6.37=h1d00b33_2 +libssh2=1.10.0=h680486a_2 +libtiff=4.3.0=hd413186_2 +libxml2=2.9.12=hf5bbc77_0 +libxslt=1.1.33=h65864e5_2 +libzlib=1.2.11=h8ffe710_1013 +lxml=4.6.3=py39h4fd7cdf_0 +lz4-c=1.9.3=h8ffe710_1 +mako=1.1.5=pyhd8ed1ab_0 +markupsafe=2.0.1=py39hb82d6ee_0 +matplotlib=3.4.3=py39hcbf5309_1 +matplotlib-base=3.4.3=py39h581301d_1 +matplotlib-inline=0.1.2=pyhd3eb1b0_2 +mkl=2021.3.0=haa95532_524 +mkl-service=2.4.0=py39h2bbff1b_0 +mkl_fft=1.3.0=py39h277e83a_2 +mkl_random=1.2.2=py39hf11a4ad_0 +numexpr=2.7.3=py39hb80d3ca_1 +numpy=1.21.2=py39hfca59bb_0 +numpy-base=1.21.2=py39h0829f74_0 +olefile=0.46=pyh9f0ad1d_1 +openjpeg=2.4.0=hb211442_1 +openssl=1.1.1l=h8ffe710_0 +pandas=1.3.3=py39h6214cd6_0 +parso=0.8.0=py_0 +pickleshare=0.7.5=pyhd3eb1b0_1003 +pillow=8.3.2=py39h916092e_0 +pip=21.2.4=py39haa95532_0 +prompt-toolkit=3.0.8=py_0 +pyfai=0.20.0=hd8ed1ab_0 +pyfai-base=0.20.0=py39h2e25243_0 +pygments=2.7.1=py_0 +pyparsing=2.4.7=pyh9f0ad1d_0 +pyqt=5.12.3=py39hcbf5309_7 +pyqt-impl=5.12.3=py39h415ef7b_7 +pyqt5-sip=4.19.18=py39h415ef7b_7 +pyqtchart=5.12=py39h415ef7b_7 +pyqtwebengine=5.12.1=py39h415ef7b_7 +pyreadline=2.1=py39hcbf5309_1004 +python=3.9.7=h6244533_1 +python-dateutil=2.8.2=pyhd3eb1b0_0 +python_abi=3.9=2_cp39 +pytz=2021.3=pyhd3eb1b0_0 +pywin32=228=py39hbaba5e8_1 +pyzmq=22.2.1=py39hd77b12b_1 +qt=5.12.9=h5909a2a_4 +qtconsole=5.1.1=pyhd8ed1ab_0 +qtpy=1.11.2=pyhd8ed1ab_0 +scipy=1.7.1=py39hbe87c03_2 +setuptools=58.0.4=py39haa95532_0 +silx=0.15.2=hd8ed1ab_0 +silx-base=0.15.2=py39h2e25243_0 +six=1.16.0=pyhd3eb1b0_0 +sqlite=3.36.0=h2bbff1b_0 +tk=8.6.11=h8ffe710_1 +tornado=6.1=py39h2bbff1b_0 +traitlets=5.0.5=py_0 +tzdata=2021a=h5d7bf9c_0 +vc=14.2=h21ff451_1 +vs2015_runtime=14.27.29016=h5e58377_2 +wcwidth=0.2.5=py_0 +wheel=0.37.0=pyhd3eb1b0_1 +wincertstore=0.2=py39haa95532_2 +xlsx2csv=0.7.8=pypi_0 +xz=5.2.5=h62dcd97_1 +zlib=1.2.11=h8ffe710_1013 +zstd=1.5.0=h6255e5f_0 diff --git a/beamtime/xanes/calib.py b/beamtime/xanes/calib.py index 6817b69..2ef6c4d 100644 --- a/beamtime/xanes/calib.py +++ b/beamtime/xanes/calib.py @@ -5,9 +5,20 @@ import os def rbkerbest(): print("ROSENBORG!<3") +#def split_xanes_scan(filename, destination=None): -def split_xanes_scan(filename, destination=None): + # with open(filename, 'r') as f: + +##Better to make a new function that loops through the files, and performing the split_xanes_scan on + + +def split_xanes_scan(root, destination=None, replace=False): + #root is the path to the beamtime-folder + #destination should be the path to the processed data + + #insert a for-loop to go through all the folders.dat-files in the folder root\xanes\raw + with open(filename, 'r') as f: lines = f.readlines() diff --git a/beamtime/xanes/io.py b/beamtime/xanes/io.py index e69de29..a818d86 100644 --- a/beamtime/xanes/io.py +++ b/beamtime/xanes/io.py @@ -0,0 +1,2 @@ +#hello +#yeah \ No newline at end of file