Merge branch 'master' of github.uio.no:rasmusvt/beamtime

This commit is contained in:
rasmusvt 2021-10-21 14:21:27 +02:00
commit 0048546767
7 changed files with 1020 additions and 99 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 pandas as pd
import numpy as np import numpy as np
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import os
def read_battsmall(path): def read_data(path, kind, options=None):
''' Reads BATTSMALL-data into a DataFrame.
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: Input:
path (required): string with path to datafile 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 # Convert from .xlsx to .csv to make readtime faster
t_prev = df.columns[0].split()[-1].strip('[]') if path.split('.')[-1] == 'xlsx':
U_prev = df.columns[1].split()[-1].strip('[]') csv_details = ''.join(path.split('.')[:-1]) + '_details.csv'
I_prev = df.columns[2].split()[-1].strip('[]') csv_summary = ''.join(path.split('.')[:-1]) + '_summary.csv'
C_prev, m_prev = df.columns[4].split()[-1].strip('[]').split('/')
if not os.path.isfile(csv_summary):
Xlsx2csv(path, outputencoding="utf-8").convert(csv_summary, sheetid=3)
# Define matrix for unit conversion for time if not os.path.isfile(csv_details):
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]} Xlsx2csv(path, outputencoding="utf-8").convert(csv_details, sheetid=4)
t_units_df = pd.DataFrame(t_units_df)
t_units_df.index = ['h', 'min', 's', 'ms']
# Define matrix for unit conversion for current if summary:
I_units_df = {'A': [1, 1000, 1000000], 'mA': [1/1000, 1, 1000], 'uA': [1/1000000, 1/1000, 1]} df = pd.read_csv(csv_summary)
I_units_df = pd.DataFrame(I_units_df) else:
I_units_df.index = ['A', 'mA', 'uA'] df = pd.read_csv(csv_details)
# 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']
elif path.split('.')[-1] == 'csv':
df = pd.read_csv(path)
return df 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. 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. For this to work, the cycling program must be set to use the counter.
Input: 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. t (optional): Unit for time data. Defaults to ms.
C (optional): Unit for specific capacity. Defaults to mAh/g. C (optional): Unit for specific capacity. Defaults to mAh/g.
I (optional): Unit for current. Defaults mA. I (optional): Unit for current. Defaults mA.
@ -89,67 +110,424 @@ def process_battsmall_data(df, units=None):
cycles: A list with cycles: A list with
''' '''
required_units = ['t', 'I', 'U', 'C'] required_options = ['splice_cycles', 'molecular_weight', 'reverse_discharge', 'units']
default_units = {'t': 'h', 'I': 'mA', 'U': 'V', 'C': 'mAh/g'} 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', '<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('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 = ['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: if not units:
units = default_units units = default_units
if units: if units:
for unit in required_units: for unit in required_units:
if unit not in units.values(): if unit not in units.keys():
units[unit] = default_units[unit] units[unit] = default_units[unit]
units['specific_capacity'] = r'{} {}'.format(units['capacity'], units['mass']) + '$^{-1}$'
# 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]
cycles.append((chg_df, dchg_df)) return units
return cycles
def plot_gc(cycles, which_cycles='all', chg=True, dchg=True, colours=None, x='C', y='U'): def get_old_units(df, kind):
fig, ax = prepare_gc_plot() 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}
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 which_cycles == 'all': if kind=='biologic':
which_cycles = [i for i, c in enumerate(cycles)]
if not colours: for column in df.columns:
chg_colour = (40/255, 70/255, 75/255) # Dark Slate Gray #28464B if 'time' in column:
dchg_colour = (239/255, 160/255, 11/255) # Marigold #EFA00B 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]
old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy, 'time': time}
for i, cycle in cycles: return old_units
if i in which_cycles:
if chg: def convert_time_string(time_string, unit='ms'):
cycle[0].plot(ax=ax) ''' 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'''
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
@ -158,18 +536,3 @@ def plot_gc(cycles, which_cycles='all', chg=True, dchg=True, colours=None, x='C'
def prepare_gc_plot(figsize=(14,7), dpi=None):
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
return fig, ax

View file

@ -1 +1,386 @@
import matplotlib.pyplot as plt 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

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

107
beamtime/requirements.txt Normal file
View file

@ -0,0 +1,107 @@
# This file may be used to create an environment using:
# $ conda create --name <env> --file <this 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

View file

@ -5,8 +5,19 @@ import os
def rbkerbest(): def rbkerbest():
print("ROSENBORG!<3") 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: with open(filename, 'r') as f:
lines = f.readlines() lines = f.readlines()

View file

@ -0,0 +1,2 @@
#hello
#yeah