This commit is contained in:
halvorhv 2021-10-14 14:18:53 +02:00
commit 9a2aa7e3ab
4 changed files with 808 additions and 166 deletions

View file

@ -1 +1 @@
from . import io, plot
from . import io, plot, unit_tables

View file

@ -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
@ -20,31 +37,55 @@ 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.'''
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
# 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)
if not os.path.isfile(csv_details):
Xlsx2csv(path, outputencoding="utf-8").convert(csv_details, sheetid=4)
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)
# 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'
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)
elif filename.split('.')[-1] == 'csv':
df = pd.read_csv(filename)
return df
return df
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
@ -52,16 +93,14 @@ def read_neware(path, summary=False, active_material_weight=None, molecular_weig
#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, 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.
@ -71,51 +110,42 @@ def process_battsmall_data(df, units=None, splice_cycles=None, molecular_weight=
cycles: A list with
'''
#########################
#### UNIT CONVERSION ####
#########################
required_options = ['splice_cycles', 'molecular_weight', 'reverse_discharge', 'units']
default_options = {'splice_cycles': None, 'molecular_weight': None, 'reverse_discharge': False, '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'}
if not units:
units = default_units
if units:
for unit in required_units:
if unit not in units.values():
units[unit] = default_units[unit]
if not options:
options = default_options
else:
for option in required_options:
if option not in options.keys():
options[option] = default_options[option]
# 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}
# 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')
# Convert all units to the desired units.
df = unit_conversion(df=df, units=units)
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]
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, 'specific_capacity'] *= -1
chg_df = sub_df.loc[chg_mask]
dchg_df = sub_df.loc[dchg_mask]
@ -124,6 +154,18 @@ def process_battsmall_data(df, units=None, splice_cycles=None, molecular_weight=
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))
@ -132,99 +174,312 @@ def process_battsmall_data(df, units=None, splice_cycles=None, molecular_weight=
return cycles
def process_neware_data(df, units=None, splice_cycles=None, active_material_weight=None, molecular_weight=None):
def process_neware_data(df, options=None):
#########################
#### 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 """
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', '<I>/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('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
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["<I>/{}".format(old_units["current"])] = df["<I>/{}".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 = ['t', 'I', 'U', 'C']
default_units = {'t': 'h', 'I': 'mA', 'U': 'V', 'C': 'mAh/g'}
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]
units['specific_capacity'] = r'{} {}'.format(units['capacity'], units['mass']) + '$^{-1}$'
# 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}
# Convert all units to the desired units.
df = unit_conversion(df=df, units=units)
return units
if active_material_weight:
df["SpecificCapacity(mAh/g)"] = df["Capacity(mAh)"] / (active_material_weight / 1000)
def get_old_units(df, kind):
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
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}
df["IonsExtracted"] = (df["SpecificCapacity(mAh/g)"]*molecular_weight)/f
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}
def unit_conversion(df, units, prev_units, kind):
if kind=='biologic':
C, m = units['C'].split('/')
C_prev, m_prev = prev_units['C'].split('/')
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 '<I>' in column:
current = column.split('/')[-1]
# 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']
old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy, 'time': time}
return df
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)}
@ -240,25 +495,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):

View file

@ -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 cycles:
if i in which_cycles:
if chg:
cycle[0].plot(ax=ax)
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)
def prepare_gc_plot(figsize=(14,7), dpi=None):
options['which_cycles'] = [i-1 for i in range(which_cycles[0], which_cycles[1]+1)]
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
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 prettify_labels(label):
labels_dict = {
'capacity': 'Capacity',
'specific_capacity': 'Specific capacity',
'voltage': 'Voltage',
'current': 'Current',
'energy': 'Energy',
}
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

View file

@ -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