From 4a987808fc720dd82e15ac385bf529d2b9703b93 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Thu, 7 Oct 2021 16:25:08 +0200 Subject: [PATCH 1/4] Update unit conversion functions --- beamtime/electrochemistry/__init__.py | 2 +- beamtime/electrochemistry/io.py | 226 +++++++++++------------ beamtime/electrochemistry/unit_tables.py | 53 ++++++ 3 files changed, 160 insertions(+), 121 deletions(-) create mode 100644 beamtime/electrochemistry/unit_tables.py 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 86964ea..3e5bf02 100644 --- a/beamtime/electrochemistry/io.py +++ b/beamtime/electrochemistry/io.py @@ -1,10 +1,11 @@ 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_batsmall(path): + ''' Reads BATSMALL-data into a DataFrame. Input: path (required): string with path to datafile @@ -21,47 +22,46 @@ def read_battsmall(path): def read_neware(path, summary=False, active_material_weight=None, molecular_weight=None): - ''' 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.''' - + ''' 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 - # Convert from .xlsx to .csv to make readtime faster - if filename.split('.')[-1] == 'xlsx': - csv_details = ''.join(filename.split('.')[:-1]) + '_details.csv' - csv_summary = ''.join(filename.split('.')[:-1]) + '_summary.csv' + # 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' - Xlsx2csv(filename, outputencoding="utf-8").convert(csv_summary, sheetid=3) - Xlsx2csv(filename, outputencoding="utf-8").convert(csv_details, sheetid=4) - - if summary: - df = pd.read_csv(csv_summary) - else: - df = pd.read_csv(csv_details) + if not os.path.isfile(csv_summary): + Xlsx2csv(path, outputencoding="utf-8").convert(csv_summary, sheetid=3) - elif filename.split('.')[-1] == 'csv': + if not os.path.isfile(csv_details): + Xlsx2csv(path, outputencoding="utf-8").convert(csv_details, sheetid=4) - df = pd.read_csv(filename) - - - return df + 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, splice_cycles=None, molecular_weight=None): - ''' Takes BATTSMALL-data in the form of a DataFrame and cleans the data up and converts units into desired units. +def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=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. @@ -71,32 +71,13 @@ def process_battsmall_data(df, units=None, splice_cycles=None, molecular_weight= cycles: A list with ''' - ######################### - #### UNIT CONVERSION #### - ######################### - # Complete the list of units - if not all are passed, then default value will be used - required_units = ['t', 'I', 'U', 'C'] - default_units = {'t': 'h', 'I': 'mA', 'U': 'V', 'C': 'mAh/g'} + # 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=units) + old_units = get_old_units(df, kind='batsmall') + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='batsmall') - if not units: - units = default_units - - if units: - for unit in required_units: - if unit not in units.values(): - units[unit] = default_units[unit] - - - # 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('/') - prev_units = {'t': t_prev, 'I': I_prev, 'U': U_prev, 'C': C_prev} - - # Convert all units to the desired units. - df = unit_conversion(df=df, units=units) + df.columns = ['TT', 'U', 'I', 'Z1', 'C', 'Comment'] # 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': ''}) @@ -138,93 +119,98 @@ def process_neware_data(df, units=None, splice_cycles=None, active_material_weig #### UNIT CONVERSION #### ######################### + # 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=units) + old_units = get_old_units(df, kind='neware') + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='neware') + + + # if active_material_weight: + # df["SpecificCapacity(mAh/g)"] = df["Capacity(mAh)"] / (active_material_weight / 1000) + + # 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(mAh/g)"]*molecular_weight)/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"]]) + + + 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['RelativeTime({})'.format(new_units['time'])] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=new_units['time']), axis=1) + + 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 = ['t', 'I', 'U', 'C'] - default_units = {'t': 'h', 'I': 'mA', 'U': 'V', 'C': 'mAh/g'} + required_units = ['time', 'current', 'voltage', 'capacity', 'mass', 'energy'] + default_units = {'time': 'h', 'current': 'mA', 'voltage': 'V', 'capacity': 'mAh', 'mass': 'g', 'energy': 'mWh'} 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] + + return units + + + +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} - # Get the units used in the data set - t_prev = 's' # default in - 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('/') - prev_units = {'t': t_prev, 'I': I_prev, 'U': U_prev, 'C': C_prev} + 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(')') - # Convert all units to the desired units. - df = unit_conversion(df=df, units=units) - - - - if active_material_weight: - df["SpecificCapacity(mAh/g)"] = df["Capacity(mAh)"] / (active_material_weight / 1000) - - 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(mAh/g)"]*molecular_weight)/f - - -def unit_conversion(df, units, prev_units, kind): - - C, m = units['C'].split('/') - C_prev, m_prev = prev_units['C'].split('/') - - - # 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'] - - # 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[C] / m_units_df[m_prev].loc[m]) - - df.columns = ['TT', 'U', 'I', 'Z1', 'C', 'Comment'] - - - - return df + old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy} + 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 = int(s)*1000 + int(m)*1000*60 + int(h)*1000*60*60 + 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)} 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 + + + From 26bd7d8a15343b7aa46848465afc6fbdba19cdbe Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Fri, 8 Oct 2021 14:02:13 +0200 Subject: [PATCH 2/4] Update io.py --- beamtime/electrochemistry/io.py | 141 ++++++++++++++++++++++++-------- 1 file changed, 106 insertions(+), 35 deletions(-) diff --git a/beamtime/electrochemistry/io.py b/beamtime/electrochemistry/io.py index 3e5bf02..0984581 100644 --- a/beamtime/electrochemistry/io.py +++ b/beamtime/electrochemistry/io.py @@ -77,26 +77,24 @@ def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=N old_units = get_old_units(df, kind='batsmall') df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='batsmall') - df.columns = ['TT', 'U', 'I', 'Z1', 'C', 'Comment'] - # 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] + 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['I'] >= 0 - dchg_mask = df['I'] < 0 + 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["Z1"].max()): + for i in range(df["count"].max()): - sub_df = df.loc[df['Z1'] == i].copy() + sub_df = df.loc[df['count'] == i].copy() - sub_df.loc[dchg_mask, 'I'] *= -1 - sub_df.loc[dchg_mask, 'C'] *= -1 + 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] @@ -113,27 +111,79 @@ def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=N return cycles -def process_neware_data(df, units=None, splice_cycles=None, active_material_weight=None, molecular_weight=None): +def process_neware_data(df, units=None, splice_cycles=None, active_material_weight=None, molecular_weight=None, reverse_discharge=False): - ######################### - #### UNIT CONVERSION #### - ######################### + """ 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 """ # 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=units) - old_units = get_old_units(df, kind='neware') + old_units = get_old_units(df=df, kind='neware') + + df = add_columns_neware(df=df, active_material_weight=active_material_weight, molecular_weight=molecular_weight, old_units=old_units) + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='neware') - # if active_material_weight: - # df["SpecificCapacity(mAh/g)"] = df["Capacity(mAh)"] / (active_material_weight / 1000) + # Creates masks for charge and discharge curves + chg_mask = df['status'] == 'CC Chg' + dchg_mask = df['status'] == 'CC DChg' - # 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 + # Initiate cycles list + cycles = [] - # df["IonsExtracted"] = (df["SpecificCapacity(mAh/g)"]*molecular_weight)/f + # 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 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_neware(df, active_material_weight, molecular_weight, old_units): + + + 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 return df @@ -149,14 +199,34 @@ def unit_conversion(df, new_units, old_units, kind): 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'] - df['RelativeTime({})'.format(new_units['time'])] = df.apply(lambda row : convert_time_string(row['Relative Time(h:min:s.ms)'], unit=new_units['time']), axis=1) + 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('run_time') + + + df.drop(['Record number', 'Relative Time(h:min:s.ms)', 'Real Time(h:min:s.ms)'], axis=1, inplace=True) + + df.columns = columns return df @@ -226,25 +296,26 @@ def convert_datetime_string(datetime_string, reference, unit='s'): from datetime import datetime # Parse the - cur_date, cur_time = datetime_string.split() - cur_y, cur_mo, cur_d = cur_date.split('-') - cur_h, cur_m, cur_s = cur_time.split(':') - cur_date = datetime(int(cur_y), int(cur_mo), int(cur_d), int(cur_h), int(cur_m), int(cur_s)) + 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)) - ref_date, ref_time = reference.split() - ref_y, ref_mo, ref_d = ref_date.split('-') - ref_h, ref_m, ref_s = ref_time.split(':') - ref_date = datetime(int(ref_y), int(ref_mo), int(ref_d), int(ref_h), int(ref_m), int(ref_s)) + 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 = cur_date - ref_date + days = current_date - reference_date - s = days.seconds + + s = days.days*24*60*60 + days.seconds factors = {'ms': 1000, 's': 1, 'min': 1/(60), 'h': 1/(60*60)} - t = s * factors[unit] + time = s * factors[unit] - return t + return time def splice_cycles(first, second): From 4f255fd9d5c47d319d9508aa40f70560c6e7b071 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Tue, 12 Oct 2021 15:53:48 +0200 Subject: [PATCH 3/4] Add BioLogic-functions to io.py --- beamtime/electrochemistry/io.py | 154 +++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 11 deletions(-) diff --git a/beamtime/electrochemistry/io.py b/beamtime/electrochemistry/io.py index 0984581..6c6e5fc 100644 --- a/beamtime/electrochemistry/io.py +++ b/beamtime/electrochemistry/io.py @@ -21,7 +21,7 @@ def read_batsmall(path): -def read_neware(path, summary=False, active_material_weight=None, molecular_weight=None): +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.''' @@ -51,6 +51,29 @@ def read_neware(path, summary=False, active_material_weight=None, molecular_weig +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 + + + + @@ -94,7 +117,7 @@ def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=N sub_df = df.loc[df['count'] == i].copy() sub_df.loc[dchg_mask, 'current'] *= -1 - sub_df.loc[dchg_mask, 'capacity'] *= -1 + sub_df.loc[dchg_mask, 'specific_capacity'] *= -1 chg_df = sub_df.loc[chg_mask] dchg_df = sub_df.loc[dchg_mask] @@ -126,7 +149,7 @@ def process_neware_data(df, units=None, splice_cycles=None, active_material_weig new_units = set_units(units=units) old_units = get_old_units(df=df, kind='neware') - df = add_columns_neware(df=df, active_material_weight=active_material_weight, molecular_weight=molecular_weight, old_units=old_units) + df = add_columns(df=df, active_material_weight=active_material_weight, molecular_weight=molecular_weight, old_units=old_units, kind='neware') df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='neware') @@ -172,19 +195,91 @@ def process_neware_data(df, units=None, splice_cycles=None, active_material_weig return cycles -def add_columns_neware(df, active_material_weight, molecular_weight, old_units): +def process_biologic_data(df, units=None, splice_cycles=None, active_material_weight=None, molecular_weight=None, reverse_discharge=False): + + # 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=units) + old_units = get_old_units(df=df, kind='biologic') + + df = add_columns(df=df, active_material_weight=active_material_weight, molecular_weight=molecular_weight, old_units=old_units, kind='biologic') + + df = unit_conversion(df=df, new_units=new_units, old_units=old_units, kind='biologic') - if active_material_weight: - df["SpecificCapacity({}/mg)".format(old_units["capacity"])] = df["Capacity({})".format(old_units['capacity'])] / (active_material_weight) + # 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) - 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 + # 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 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 @@ -228,6 +323,25 @@ def unit_conversion(df, new_units, old_units, kind): 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 @@ -274,6 +388,24 @@ def get_old_units(df, kind): 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'): From 43e6ef27c842a9e93152ad2a57f695c60afccb13 Mon Sep 17 00:00:00 2001 From: rasmusvt Date: Wed, 13 Oct 2021 18:06:56 +0200 Subject: [PATCH 4/4] Add plot functionality to electrochemistry --- beamtime/electrochemistry/io.py | 91 +++++++- beamtime/electrochemistry/plot.py | 375 ++++++++++++++++++++++++++++-- 2 files changed, 433 insertions(+), 33 deletions(-) diff --git a/beamtime/electrochemistry/io.py b/beamtime/electrochemistry/io.py index 6c6e5fc..35d271b 100644 --- a/beamtime/electrochemistry/io.py +++ b/beamtime/electrochemistry/io.py @@ -4,6 +4,22 @@ import matplotlib.pyplot as plt import os +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. @@ -77,7 +93,7 @@ def read_biologic(path): -def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=None): +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. @@ -94,12 +110,24 @@ def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=N cycles: A list with ''' + 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=units) + 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] @@ -126,6 +154,18 @@ def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=N 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)) @@ -134,7 +174,7 @@ def process_batsmall_data(df, units=None, splice_cycles=None, molecular_weight=N return cycles -def process_neware_data(df, units=None, splice_cycles=None, active_material_weight=None, molecular_weight=None, reverse_discharge=False): +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. @@ -145,14 +185,27 @@ def process_neware_data(df, units=None, splice_cycles=None, active_material_weig 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=units) + new_units = set_units(units=options['units']) old_units = get_old_units(df=df, kind='neware') - df = add_columns(df=df, active_material_weight=active_material_weight, molecular_weight=molecular_weight, old_units=old_units, 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' @@ -176,7 +229,7 @@ def process_neware_data(df, units=None, splice_cycles=None, active_material_weig if chg_df.empty and dchg_df.empty: continue - if reverse_discharge: + if options['reverse_discharge']: max_capacity = dchg_df['capacity'].max() dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) @@ -195,19 +248,31 @@ def process_neware_data(df, units=None, splice_cycles=None, active_material_weig return cycles -def process_biologic_data(df, units=None, splice_cycles=None, active_material_weight=None, molecular_weight=None, reverse_discharge=False): +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=units) + new_units = set_units(units=options['units']) old_units = get_old_units(df=df, kind='biologic') - df = add_columns(df=df, active_material_weight=active_material_weight, molecular_weight=molecular_weight, old_units=old_units, 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) @@ -233,7 +298,7 @@ def process_biologic_data(df, units=None, splice_cycles=None, active_material_we if chg_df.empty and dchg_df.empty: continue - if reverse_discharge: + if options['reverse_discharge']: max_capacity = dchg_df['capacity'].max() dchg_df['capacity'] = np.abs(dchg_df['capacity'] - max_capacity) @@ -348,8 +413,8 @@ def unit_conversion(df, new_units, old_units, kind): 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'] - default_units = {'time': 'h', 'current': 'mA', 'voltage': 'V', 'capacity': 'mAh', 'mass': 'g', 'energy': 'mWh'} + 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 @@ -359,6 +424,8 @@ def set_units(units=None): if unit not in units.keys(): units[unit] = default_units[unit] + units['specific_capacity'] = r'{} {}'.format(units['capacity'], units['mass']) + '$^{-1}$' + return units diff --git a/beamtime/electrochemistry/plot.py b/beamtime/electrochemistry/plot.py index 70f2b22..eabd105 100644 --- a/beamtime/electrochemistry/plot.py +++ b/beamtime/electrochemistry/plot.py @@ -1,40 +1,373 @@ 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(cycles, which_cycles='all', chg=True, dchg=True, colours=None, x='C', y='U'): +def plot_gc(path, kind, options=None): - fig, ax = prepare_gc_plot() + # 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) - if which_cycles == 'all': - which_cycles = [i for i, c in enumerate(cycles)] + # Update options + required_options = ['x_vals', 'y_vals', 'which_cycles', 'chg', 'dchg', 'colours', 'gradient'] + default_options = {'x_vals': 'capacity', 'y_vals': 'voltage', 'which_cycles': 'all', 'chg': True, 'dchg': True, 'colours': None, 'gradient': False} - 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 + 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) + + print(len(options['which_cycles'])) + print(len(colours)) + + + + 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']) - for i, cycle in cycles: - if i in which_cycles: - if chg: - cycle[0].plot(ax=ax) + 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', + '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, + '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']: + print(options['x_vals']) + print(options['units']) + 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 + + default_ticks = { + 'specific_capacity': [100, 50], + 'capacity': [0.1, 0.05], + 'voltage': [0.5, 0.25] + } + + + + # 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']) + + ################################################################## + ############################# TITLE ############################## + ################################################################## + + if options['title']: + ax.set_title(options['title'], size=options['sizes']['title']) + + ################################################################## + ############################# LEGEND ############################# + ################################################################## + + ax.get_legend().remove() + + return fig, ax - - - - - - -def prepare_gc_plot(figsize=(14,7), dpi=None): - - fig, ax = plt.subplots(figsize=figsize, dpi=dpi) +def prettify_labels(label): + labels_dict = { + 'capacity': 'Capacity', + 'specific_capacity': 'Specific capacity', + 'voltage': 'Voltage', + 'current': 'Current', + 'energy': 'Energy', + } - return fig, ax \ No newline at end of file + 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 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