Rename package to nafuma from beamtime

This commit is contained in:
rasmusvt 2022-04-07 17:05:52 +02:00
parent 049f30d96b
commit 27c911cf54
25 changed files with 5 additions and 7 deletions

1
nafuma/__init__.py Normal file
View file

@ -0,0 +1 @@

55
nafuma/auxillary.py Normal file
View file

@ -0,0 +1,55 @@
import json
import numpy as np
def update_options(options, required_options, default_options):
''' Takes a dictionary of options along with a list of required options and dictionary of default options, and sets all keyval-pairs of options that is not already defined to the default values'''
for option in required_options:
if option not in options.keys():
options[option] = default_options[option]
return options
def save_options(options, path):
''' Saves any options dictionary to a JSON-file in the specified path'''
with open(path, 'w') as f:
json.dump(options,f)
def load_options(path):
''' Loads JSON-file into a dictionary'''
with open(path, 'r') as f:
options = json.load(f)
return(options)
def swap_values(dict, key1, key2):
key1_val = dict[key1]
dict[key1] = dict[key2]
dict[key2] = key1_val
return dict
def ceil(a, roundto=1):
fac = 1/roundto
a = np.ceil(a*fac) / fac
return a
def floor(a, roundto=1):
fac = 1/roundto
a = np.floor(a*fac) / fac
return a

View file

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

View file

@ -0,0 +1,582 @@
import pandas as pd
import numpy as np
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.
Input:
path (required): string with path to datafile
Output:
df: pandas DataFrame containing the data as-is, but without additional NaN-columns.'''
df = pd.read_csv(path, skiprows=2, sep='\t')
df = df.loc[:, ~df.columns.str.contains('^Unnamed')]
return df
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)
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
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 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.
U (optional): Unit for voltage. Defaults to V.
Output:
cycles: A list with
'''
required_options = ['splice_cycles', 'molecular_weight', 'reverse_discharge', 'units']
default_options = {'splice_cycles': False, '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
if options['splice_cycles']:
df = splice_cycles(df=df, kind='batsmall')
# 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+1].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 splice_cycles(df, kind):
if kind == 'batsmall':
# Creates masks for charge and discharge curves
chg_mask = df['current'] >= 0
dchg_mask = df['current'] < 0
# Get the number of cycles in the dataset
max_count = df["count"].max()
# 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+1]
sub_df_chg = sub_df.loc[chg_mask]
#sub_df_dchg = sub_df.loc[dchg_mask]
# get indices where the program changed
chg_indices = sub_df_chg[sub_df_chg["comment"].str.contains("program")==True].index.to_list()
# Delete first item if first cycle after rest (this will just be the start of the cycling)
if i+1 == 1:
del chg_indices[0]
if chg_indices:
last_chg = chg_indices.pop()
#dchg_indices = sub_df_dchg[sub_df_dchg["comment"].str.contains("program")==True].index.to_list()
#if dchg_indices:
# del dchg_indices[0]
if chg_indices:
for i in chg_indices:
add = df['specific_capacity'].iloc[i-1]
df['specific_capacity'].iloc[i:last_chg] = df['specific_capacity'].iloc[i:last_chg] + add
#if dchg_indices:
# for i in dchg_indices:
# add = df['specific_capacity'].iloc[i-1]
# df['specific_capacity'].iloc[i:last_dchg] = df['specific_capacity'].iloc[i:last_dchg] + add
return df
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+1].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:
units = default_units
if units:
for unit in required_units:
if unit not in units.keys():
units[unit] = default_units[unit]
units['specific_capacity'] = r'{} {}'.format(units['capacity'], units['mass']) + '$^{-1}$'
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}
if kind=='neware':
for column in df.columns:
if 'Voltage' in column:
voltage = column.split('(')[-1].strip(')')
elif 'Current' in column:
current = column.split('(')[-1].strip(')')
elif 'Capacity' in column:
capacity = column.split('(')[-1].strip(')')
elif 'Energy' in column:
energy = column.split('(')[-1].strip(')')
old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy}
if kind=='biologic':
for column in df.columns:
if 'time' in column:
time = column.split('/')[-1]
elif 'Ewe' in column:
voltage = column.split('/')[-1]
elif 'Capacity' in column:
capacity = column.split('/')[-1].replace('.', '')
elif 'Energy' in column:
energy = column.split('/')[-1].replace('.', '')
elif '<I>' in column:
current = column.split('/')[-1]
old_units = {'voltage': voltage, 'current': current, 'capacity': capacity, 'energy': energy, 'time': time}
return old_units
def convert_time_string(time_string, unit='ms'):
''' Convert time string from Neware-data with the format hh:mm:ss.xx to any given unit'''
h, m, s = time_string.split(':')
ms = float(s)*1000 + int(m)*1000*60 + int(h)*1000*60*60
factors = {'ms': 1, 's': 1/1000, 'min': 1/(1000*60), 'h': 1/(1000*60*60)}
t = ms*factors[unit]
return t
def convert_datetime_string(datetime_string, reference, unit='s'):
''' Convert time string from Neware-data with the format yyy-mm-dd hh:mm:ss to any given unit'''
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

View file

@ -0,0 +1,387 @@
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_ytick = options['yticks'][0]
minor_ytick = 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 #############################
##################################################################
if ax.get_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

0
nafuma/pdf/__init__.py Normal file
View file

342
nafuma/plotting.py Normal file
View file

@ -0,0 +1,342 @@
import beamtime.auxillary as aux
import matplotlib.pyplot as plt
from matplotlib.ticker import (MultipleLocator)
import importlib
import matplotlib.patches as mpatches
from matplotlib.lines import Line2D
import matplotlib.lines as mlines
import itertools
import numpy as np
def prepare_plot(options={}):
''' A general function to prepare a plot based on contents of options['rc_params'] and options['format_params'].
rc_params is a dictionary with keyval-pairs corresponding to rcParams in matplotlib, to give the user full control over this. Please consult the matplotlib-documentation
format_params will determine the size, aspect ratio, resolution etc. of the figure. Should be modified to conform with any requirements from a journal.'''
if 'rc_params' in options.keys():
rc_params = options['rc_params']
else:
rc_params = {}
if 'format_params' in options.keys():
format_params = options['format_params']
else:
format_params = {}
required_format_params = ['single_column_width', 'double_column_width', 'column_type', 'width_ratio', 'aspect_ratio',
'width', 'height', 'compress_width', 'compress_height', 'upscaling_factor', 'dpi',
'nrows', 'ncols', 'grid_ratio_height', 'grid_ratio_width']
default_format_params = {
'single_column_width': 8.3,
'double_column_width': 17.1,
'column_type': 'single',
'width_ratio': '1:1',
'aspect_ratio': '1:1',
'width': None,
'height': None,
'compress_width': 1,
'compress_height': 1,
'upscaling_factor': 1.0,
'dpi': 600,
'nrows': 1,
'ncols': 1,
'grid_ratio_height': None,
'grid_ratio_width': None
}
format_params = aux.update_options(format_params, required_format_params, default_format_params)
# Reset run commands
plt.rcdefaults()
# Update run commands if any is passed (will pass an empty dictionary if not passed)
update_rc_params(rc_params)
if not format_params['width']:
format_params['width'] = determine_width(format_params=format_params)
if not format_params['height']:
format_params['height'] = determine_height(format_params=format_params, width=format_params['width'])
format_params['width'], format_params['height'] = scale_figure(format_params=format_params, width=format_params['width'], height=format_params['height'])
if format_params['nrows'] == 1 and format_params['ncols'] == 1:
fig, ax = plt.subplots(figsize=(format_params['width'], format_params['height']), dpi=format_params['dpi'])
return fig, ax
else:
if not format_params['grid_ratio_height']:
format_params['grid_ratio_height'] = [1 for i in range(format_params['nrows'])]
if not format_params['grid_ratio_width']:
format_params['grid-ratio_width'] = [1 for i in range(format_params['ncols'])]
fig, axes = plt.subplots(nrows=format_params['nrows'], ncols=format_params['ncols'], figsize=(format_params['width'],format_params['height']),
gridspec_kw={'height_ratios': format_params['grid_ratio_height'], 'width_ratios': format_params['grid_ratio_width']},
facecolor='w', dpi=format_params['dpi'])
return fig, axes
def adjust_plot(fig, ax, options):
''' A general function to adjust plot according to contents of the options-dictionary '''
required_options = [
'plot_kind',
'xlabel', 'ylabel',
'xunit', 'yunit',
'hide_x_labels', 'hide_y_labels',
'hide_x_ticklabels', 'hide_y_ticklabels',
'hide_x_ticks', 'hide_y_ticks',
'x_tick_locators', 'y_tick_locators',
'rotation_x_ticks', 'rotation_y_ticks',
'xticks', 'yticks',
'xlim', 'ylim',
'title',
'legend', 'legend_position', 'legend_ncol',
'subplots_adjust',
'text']
default_options = {
'plot_kind': None, # defaults to None, but should be utilised when requiring special formatting for a particular plot
'xlabel': None, 'ylabel': None,
'xunit': None, 'yunit': None,
'hide_x_labels': False, 'hide_y_labels': False, # Whether the main labels on the x- and/or y-axes should be hidden
'hide_x_ticklabels': False, 'hide_y_ticklabels': False, # Whether ticklabels on the x- and/or y-axes should be hidden
'hide_x_ticks': False, 'hide_y_ticks': False, # Whether the ticks on the x- and/or y-axes should be hidden
'x_tick_locators': None, 'y_tick_locators': None, # The major and minor tick locators for the x- and y-axes
'rotation_x_ticks': 0, 'rotation_y_ticks': 0, # Degrees the x- and/or y-ticklabels should be rotated
'xticks': None, 'yticks': None, # Custom definition of the xticks and yticks. This is not properly implemented now.
'xlim': None, 'ylim': None, # Limits to the x- and y-axes
'title': None, # Title of the plot
'legend': False, 'legend_position': ['lower center', (0.5, -0.1)], 'legend_ncol': 1, # Toggles on/off legend. Specifices legend position and the number of columns the legend should appear as.
'subplots_adjust': [0.1, 0.1, 0.9, 0.9], # Adjustment of the Axes-object within the Figure-object. Fraction of the Figure-object the left, bottom, right and top edges of the Axes-object will start.
'text': None # Text to show in the plot. Should be a list where the first element is the string, and the second is a tuple with x- and y-coordinates. Could also be a list of lists to show more strings of text.
}
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
# Set labels on x- and y-axes
if not options['hide_y_labels']:
ax.set_ylabel(f'{options["ylabel"]} [{options["yunit"]}]')
else:
ax.set_ylabel('')
if not options['hide_x_labels']:
ax.set_xlabel(f'{options["xlabel"]}')
else:
ax.set_xlabel('')
# Set multiple locators
if options['y_tick_locators']:
ax.yaxis.set_major_locator(MultipleLocator(options['y_tick_locators'][0]))
ax.yaxis.set_minor_locator(MultipleLocator(options['y_tick_locators'][1]))
if options['x_tick_locators']:
ax.xaxis.set_major_locator(MultipleLocator(options['x_tick_locators'][0]))
ax.xaxis.set_minor_locator(MultipleLocator(options['x_tick_locators'][1]))
# FIXME THIS NEEDS REWORK FOR IT TO FUNCTION PROPERLY!
#if options['xticks']:
# ax.set_xticks(np.arange(plot_data['start'], plot_data['end']+1))
# ax.set_xticklabels(options['xticks'])
# else:
# ax.set_xticks(np.arange(plot_data['start'], plot_data['end']+1))
# ax.set_xticklabels([x/2 for x in np.arange(plot_data['start'], plot_data['end']+1)])
# Hide x- and y- ticklabels
if options['hide_y_ticklabels']:
ax.tick_params(axis='y', direction='in', which='both', labelleft=False, labelright=False)
else:
plt.xticks(rotation=options['rotation_x_ticks'])
#ax.set_xticklabels(ax.get_xticks(), rotation = options['rotation_x_ticks'])
if options['hide_x_ticklabels']:
ax.tick_params(axis='x', direction='in', which='both', labelbottom=False, labeltop=False)
else:
pass
#ax.set_yticklabels(ax.get_yticks(), rotation = options['rotation_y_ticks'])
# Hide x- and y-ticks:
if options['hide_y_ticks']:
ax.tick_params(axis='y', direction='in', which='both', left=False, right=False)
if options['hide_x_ticks']:
ax.tick_params(axis='x', direction='in', which='both', bottom=False, top=False)
# Set title
if options['title']:
ax.set_title(options['title'], fontsize=plt.rcParams['font.size'])
# Create legend
if ax.get_legend():
ax.get_legend().remove()
if options['legend']:
# Make palette and linestyles from original parameters
if not options['colours']:
colours = generate_colours(palettes=options['palettes'])
else:
colours = itertools.cycle(options['colours'])
markers = itertools.cycle(options['markers'])
# Create legend
active_markers = []
active_labels = []
for label in options['labels']:
# Discard next linestyle and colour if label is _
if label == '_':
_ = next(colours)
_ = next(markers)
else:
active_markers.append(mlines.Line2D([], [], markeredgecolor=next(colours), color=(1, 1, 1, 0), marker=next(markers)))
active_labels.append(label)
ax.legend(active_markers, active_labels, frameon=False, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], ncol=options['legend_ncol'])
#fig.legend(handles=patches, loc=options['legend_position'][0], bbox_to_anchor=options['legend_position'][1], frameon=False)
# Adjust where the axes start within the figure. Default value is 10% in from the left and bottom edges. Used to make room for the plot within the figure size (to avoid using bbox_inches='tight' in the savefig-command, as this screws with plot dimensions)
plt.subplots_adjust(left=options['subplots_adjust'][0], bottom=options['subplots_adjust'][1], right=options['subplots_adjust'][2], top=options['subplots_adjust'][3])
# If limits for x- and y-axes is passed, sets these.
if options['xlim'] is not None:
ax.set_xlim(options['xlim'])
if options['ylim'] is not None:
ax.set_ylim(options['ylim'])
# Add custom text
if options['text']:
# If only a single element, put it into a list so the below for-loop works.
if isinstance(options['text'][0], str):
options['text'] = [options['text']]
# Plot all passed texts
for text in options['text']:
plt.text(x=text[1][0], y=text[1][1], s=text[0])
return fig, ax
def ipywidgets_update(func, data, options={}, **kwargs):
''' A general ipywidgets update function that can be passed to ipywidgets.interactive. To use this, you can run:
import ipywidgets as widgets
import beamtime.plotting as btp
w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(my_func), plot_data=widgets.fixed(plot_data), options=widgets.fixed(options), key1=widget1, key2=widget2, key3=widget3)
where key1, key2, key3 etc. are the values in the options-dictionary you want widget control of, and widget1, widget2, widget3 etc. are widgets to control these values, e.g. widgets.IntSlider(value=1, min=0, max=10)
'''
# Update the options-dictionary with the values from the widgets
for key in kwargs:
options[key] = kwargs[key]
# Call the function with the plot_data and options-dictionaries
func(data=data, options=options)
def determine_width(format_params):
''' '''
conversion_cm_inch = 0.3937008 # cm to inch
if format_params['column_type'] == 'single':
column_width = format_params['single_column_width']
elif format_params['column_type'] == 'double':
column_width = format_params['double_column_width']
column_width *= conversion_cm_inch
width_ratio = [float(num) for num in format_params['width_ratio'].split(':')]
width = column_width * width_ratio[0]/width_ratio[1]
return width
def determine_height(format_params, width):
aspect_ratio = [float(num) for num in format_params['aspect_ratio'].split(':')]
height = width/(aspect_ratio[0] / aspect_ratio[1])
return height
def scale_figure(format_params, width, height):
width = width * format_params['upscaling_factor'] * format_params['compress_width']
height = height * format_params['upscaling_factor'] * format_params['compress_height']
return width, height
def update_rc_params(rc_params):
''' Update all passed run commands in matplotlib'''
if rc_params:
for key in rc_params.keys():
plt.rcParams.update({key: rc_params[key]})
def generate_colours(palettes, kind=None):
if kind == 'single':
colour_cycle = itertools.cycle(palettes)
else:
# Creates a list of all the colours that is passed in the colour_cycles argument. Then makes cyclic iterables of these.
colour_collection = []
for palette in palettes:
mod = importlib.import_module("palettable.colorbrewer.%s" % palette[0])
colour = getattr(mod, palette[1]).mpl_colors
colour_collection = colour_collection + colour
colour_cycle = itertools.cycle(colour_collection)
return colour_cycle

1
nafuma/test.txt Normal file
View file

@ -0,0 +1 @@
sdfsdfsdfsdf

0
nafuma/test/__init__.py Normal file
View file

9
nafuma/test/pytest.ini Normal file
View file

@ -0,0 +1,9 @@
# pytest.ini
[pytest]
minversion = 6.0
testpaths =
.
filterwarnings =
ignore::DeprecationWarning

View file

@ -0,0 +1,78 @@
import beamtime.auxillary as aux
import os
def test_swap_values():
dict = {'test1': 1, 'test2': 2}
key1 = 'test1'
key2 = 'test2'
oldval1 = dict[key1]
oldval2 = dict[key2]
new_dict = aux.swap_values(dict=dict, key1=key1, key2=key2)
assert (dict[key1] == oldval2) and (dict[key2] == oldval1)
def test_ceil() -> None:
assert aux.ceil(1.05, 0.5) == 1.5
assert aux.ceil(1.05, 1) == 2.0
assert aux.ceil(1.1, 0.2) == 1.2
def test_floor() -> None:
assert aux.floor(2.02, 1) == 2.0
assert aux.floor(2.02, 0.01) == 2.02
assert aux.floor(2.013, 0.01) == 2.01
def test_options() -> None:
options = {}
required_options = ['test1', 'test2', 'test3', 'test4']
default_options = {
'test1': 1,
'test2': 2,
'test3': 3,
'test4': 4,
'test5': 5,
}
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
assert options['test1'] == default_options['test1']
assert len(options.items()) == len(required_options)
assert 'test5' not in options.keys()
def test_save_options() -> None:
options = {'test1': 1, 'test2': 2}
path = 'tmp.dat'
aux.save_options(options, path)
assert os.path.isfile(path)
os.remove(path)
def test_load_options() -> None:
options = {'test1': 1, 'test2': 2}
path = 'tmp.dat'
aux.save_options(options, path)
loaded_options = aux.load_options(path)
assert (loaded_options['test1'] == 1) and (loaded_options['test2'] == 2)
os.remove(path)

View file

@ -0,0 +1,181 @@
import beamtime.plotting as btp
from cycler import cycler
import itertools
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
def test_generate_colours() -> None:
assert type(btp.generate_colours('black', kind='single')) == itertools.cycle
palettes = [('qualitative', 'Dark2_8')]
colour_cycle = btp.generate_colours(palettes)
assert type(colour_cycle) == itertools.cycle
# Test that it actually loaded 8 colours when given a set of 8 colours to
same_colour = None
for i in range(10):
colour = next(colour_cycle)
if i == 0:
first_colour = colour
if colour == first_colour:
repeat_colour_index = i
assert repeat_colour_index == 8
def test_update_rc_params() -> None:
rc_params = {
'lines.linewidth': 100
}
prev_params = plt.rcParams['lines.linewidth']
# Update run commands if any is passed (will pass an empty dictionary if not passed)
btp.update_rc_params(rc_params)
new_params = plt.rcParams['lines.linewidth']
assert new_params == 100
assert prev_params != new_params
# Reset run commands
plt.rcdefaults()
def test_scale_figure() -> None:
width, height = 1, 1
format_params = {
'upscaling_factor': 2,
'compress_width': 1,
'compress_height': 1
}
width1, height1 = btp.scale_figure(format_params=format_params, width=width, height=height)
assert width1 == 2 and height1 == 2
format_params = {
'upscaling_factor': 1,
'compress_width': 0.5,
'compress_height': 1
}
width2, height2 = btp.scale_figure(format_params=format_params, width=width, height=height)
assert width2 == 0.5 and height2 == 1
format_params = {
'upscaling_factor': 2,
'compress_width': 0.5,
'compress_height': 0.2
}
width2, height2 = btp.scale_figure(format_params=format_params, width=width, height=height)
assert width2 == 1 and height2 == 0.4
def test_determine_width() -> None:
conversion_cm_inch = 0.3937008 # cm to inch
format_params = {
'column_type': 'single',
'single_column_width': 5,
'double_column_width': 10,
'width_ratio': '1:1'
}
assert np.round(btp.determine_width(format_params),6) == np.round(5*conversion_cm_inch,6)
format_params['column_type'] = 'double'
assert np.round(btp.determine_width(format_params), 6) == np.round(10*conversion_cm_inch, 6)
format_params['column_type'] = 'single'
format_params['width_ratio'] = '1:2'
assert np.round(btp.determine_width(format_params), 6) == np.round(2.5*conversion_cm_inch, 6)
def test_determine_height() -> None:
width = 1
format_params = {
'aspect_ratio': '1:1'
}
assert btp.determine_height(format_params=format_params, width=width) == 1
format_params['aspect_ratio'] = '3:1'
assert (btp.determine_height(format_params=format_params, width=width) - 0.333333333333333) < 10e-7
assert True
def test_prepare_plot() -> None:
fig, ax = btp.prepare_plot()
assert type(fig) == plt.Figure
assert fig.get_dpi() == 600
assert ax.get_xlim() == (0.0, 1.0)
def test_adjust_plot() -> None:
fig, ax = btp.prepare_plot()
options = {
'xlim': (0.0, 2.0),
'title': 'Test'
}
fig, ax = btp.adjust_plot(fig, ax, options)
assert ax.get_xlim() == (0.0, 2.0)
assert ax.get_title() == 'Test'
def test_ipywidgets_update() -> None:
def test_func(data, options):
test1 = options['test1']
test2 = options['test2']
assert type(data) == dict
assert test1 == 1
assert test2 == 2
data = {}
options = {}
btp.ipywidgets_update(func=test_func, data=data, options=options, test1=1, test2=2)

View file

View file

0
nafuma/test2.txt Normal file
View file

1
nafuma/xanes/__init__.py Normal file
View file

@ -0,0 +1 @@
from . import io, calib

94
nafuma/xanes/calib.py Normal file
View file

@ -0,0 +1,94 @@
import pandas as pd
import numpy as np
import os
def rbkerbest():
print("ROSENBORG!<3")
#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(filename, destination=None, replace=False):
#root is the path to the beamtime-folder
#destination should be the path to the processed data
#insert a for-loop to go through all the folders.dat-files in the folder root\xanes\raw
with open(filename, 'r') as f:
lines = f.readlines()
datas = []
data = []
headers = []
header = ''
start = False
for line in lines:
if line[0:2] == "#L":
start = True
header = line[2:].split()
continue
elif line[0:2] == "#C":
start = False
if data:
datas.append(data)
data = []
if header:
headers.append(header)
header = ''
if start == False:
continue
else:
data.append(line.split())
edges = {'Mn': [6.0, 6.1, 6.2, 6.3, 6.4, 6.5], 'Fe': [6.8, 6.9, 7.0, 7.1, 7.2], 'Co': [7.6, 7.7, 7.8, 7.9], 'Ni': [8.1, 8.2, 8.3, 8.4, 8.5]}
edge_count = {'Mn': 0, 'Fe': 0, 'Co': 0, 'Ni': 0}
for ind, data in enumerate(datas):
df = pd.DataFrame(data)
df.columns = headers[ind]
edge_start = np.round((float(df["ZapEnergy"].min())), 1)
for edge, energies in edges.items():
if edge_start in energies:
edge_actual = edge
edge_count[edge] += 1
filename = filename.split('/')[-1]
count = str(edge_count[edge_actual]).zfill(4)
# Save
if destination:
cwd = os.getcwd()
if not os.path.isdir(destination):
os.mkdir(destination)
os.chdir(destination)
df.to_csv('{}_{}_{}.dat'.format(filename.split('.')[0], edge_actual, count))
os.chdir(cwd)
else:
df.to_csv('{}_{}_{}.dat'.format(filename.split('.')[0], edge_actual, count))

2
nafuma/xanes/io.py Normal file
View file

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

1
nafuma/xrd/__init__.py Normal file
View file

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

533
nafuma/xrd/io.py Normal file
View file

@ -0,0 +1,533 @@
from sympy import re
import fabio, pyFAI
import pandas as pd
import numpy as np
import os
import shutil
import zipfile
import xml.etree.ElementTree as ET
import beamtime.auxillary as aux
def get_image_array(path):
image = fabio.open(path)
image_array = image.data
return image_array
def get_image_headers(path):
image = fabio.open(path)
return image.header
def integrate_1d(data, options={}, index=0):
''' Integrates an image file to a 1D diffractogram.
Required content of data:
calibrant (str): path to .poni-file
nbins (int): Number of bins to divide image into
path (str) (optional, dependent on image): path to image file - either this or image must be specified. If both is passed, image is prioritsed
image (NumPy 2D Array) (optional, dependent on path): image array as extracted from get_image_array
Output:
df: DataFrame contianing 1D diffractogram if option 'return' is True
'''
required_options = ['unit', 'nbins', 'save', 'save_filename', 'save_extension', 'save_folder', 'overwrite', 'extract_folder']
default_options = {
'unit': '2th_deg',
'nbins': 3000,
'extract_folder': 'tmp',
'save': False,
'save_filename': None,
'save_extension': '_integrated.xy',
'save_folder': '.',
'overwrite': False}
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
if not isinstance(data['path'], list):
data['path'] = [data['path']]
# Get image array from filename if not passed
if 'image' not in data.keys():
data['image'] = get_image_array(data['path'][index])
# Instanciate the azimuthal integrator from pyFAI from the calibrant (.poni-file)
ai = pyFAI.load(data['calibrant'])
# Determine filename
filename = make_filename(options=options, path=data['path'][index])
# Make save_folder if this does not exist already
if not os.path.isdir(options['extract_folder']):
os.makedirs(options['extract_folder'])
res = ai.integrate1d(data['image'], options['nbins'], unit=options['unit'], filename=filename)
data['path'][index] = filename
diffractogram, wavelength = read_xy(data=data, options=options, index=index)
if not options['save']:
os.remove(filename)
shutil.rmtree(f'tmp')
# Reset this option
options['save_folder'] = None
return diffractogram, wavelength
def make_filename(options, path=None):
# Define save location for integrated diffractogram data
if not options['save']:
filename = os.path.join(options['extract_folder'], 'tmp_diff.dat')
elif options['save']:
# Case 1: No filename is given.
if not options['save_filename']:
# If a path is given instead of an image array, the path is taken as the trunk of the savename
if path:
# Make filename by joining the save_folder, the filename (with extension deleted) and adding the save_extension
filename = os.path.join(options['save_folder'], os.path.split(path)[-1].split('.')[0] + options['save_extension'])
else:
# Make filename just "integrated.dat" in the save_folder
filename = os.path.join(options['save_folder'], 'integrated.xy')
else:
filename = os.path.join(options['save_folder'], options['save_filename'])
if not options['overwrite']:
trunk = filename.split('.')[0]
extension = filename.split('.')[-1]
counter = 0
while os.path.isfile(filename):
# Rename first file to match naming scheme if already exists
if counter == 0:
os.rename(filename, trunk + '_' + str(counter).zfill(4) + '.' + extension)
# Increment counter and make new filename
counter += 1
counter_string = str(counter)
filename = trunk + '_' + counter_string.zfill(4) + '.' + extension
return filename
def generate_image_list(path, options=None):
''' Generates a list of paths to pass to the average_images() function'''
required_options = ['scans_per_image']
default_options = {
'scans_per_image': 5
}
def average_images(images):
''' Takes a list of path to image files, reads them and averages them before returning the average image'''
image_arrays = []
for image in images:
image_array = get_image_array(image)
image_arrays.append(image_array)
image_arrays = np.array(image_arrays)
image_average = image_arrays.mean(axis=0)
return image_average
def subtract_dark(image, dark):
return image - dark
def view_integrator(calibrant):
''' Prints out information about the azimuthal integrator
Input:
calibrant: Path to the azimuthal integrator file (.PONI)
Output:
None'''
ai = pyFAI.load(calibrant)
print("pyFAI version:", pyFAI.version)
print("\nIntegrator: \n", ai)
def read_brml(data, options={}, index=0):
# FIXME: Can't read RECX1-data, apparently must be formatted differently from RECX2. Check the RawData-files and compare between the two files.
required_options = ['extract_folder', 'save_folder']
default_options = {
'extract_folder': 'tmp',
'save_folder': None
}
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
if not os.path.isdir(options['extract_folder']):
os.mkdir(options['extract_folder'])
# Extract the RawData0.xml file from the brml-file
with zipfile.ZipFile(data['path'][index], 'r') as brml:
for info in brml.infolist():
if "RawData" in info.filename:
brml.extract(info.filename, options['extract_folder'])
# Parse the RawData0.xml file
path = os.path.join(options['extract_folder'], 'Experiment0/RawData0.xml')
tree = ET.parse(path)
root = tree.getroot()
shutil.rmtree(options['extract_folder'])
diffractogram = []
for chain in root.findall('./DataRoutes/DataRoute'):
# Get the scan type to be able to handle different data formats
scantype = chain.findall('ScanInformation')[0].get('VisibleName')
# Check if the chain is the right one to extract the data from
if chain.get('Description') == 'Originally measured data.':
if scantype == 'TwoTheta':
for scandata in chain.findall('Datum'):
scandata = scandata.text.split(',')
twotheta, intensity = float(scandata[2]), float(scandata[3])
if twotheta > 0:
diffractogram.append({'2th': twotheta, 'I': intensity})
elif scantype == 'Coupled TwoTheta/Theta':
for scandata in chain.findall('Datum'):
scandata = scandata.text.split(',')
twotheta, intensity = float(scandata[2]), float(scandata[4])
if twotheta > 0:
diffractogram.append({'2th': twotheta, 'I': intensity})
elif scantype == 'Still (Eiger2R_500K (1D mode))':
start = float(chain.findall('ScanInformation/ScaleAxes/ScaleAxisInfo/Start')[0].text)
stop = float(chain.findall('ScanInformation/ScaleAxes/ScaleAxisInfo/Stop')[0].text)
for scandata in chain.findall('Datum'):
scandata = scandata.text.split(',')
raw = [float(i) for i in scandata]
intensity = []
for r in raw:
if r > 601:
intensity.append(r)
intensity = np.array(intensity)
twotheta = np.linspace(start, stop, len(intensity))
diffractogram = {'2th': twotheta, 'I': intensity}
#if 'wavelength' not in data.keys():
# Find wavelength
for chain in root.findall('./FixedInformation/Instrument/PrimaryTracks/TrackInfoData/MountedOptics/InfoData/Tube/WaveLengthAlpha1'):
wavelength = float(chain.attrib['Value'])
diffractogram = pd.DataFrame(diffractogram)
if options['save_folder']:
if not os.path.isdir(options['save_folder']):
os.makedirs(options['save_folder'])
diffractogram.to_csv(options['save_folder'])
return diffractogram, wavelength
def read_xy(data, options={}, index=0):
#if 'wavelength' not in data.keys():
# Get wavelength from scan
wavelength = find_wavelength_from_xy(path=data['path'][index])
with open(data['path'][index], 'r') as f:
position = 0
current_line = f.readline()
while current_line[0] == '#' or current_line[0] == '\'':
position = f.tell()
current_line = f.readline()
f.seek(position)
diffractogram = pd.read_csv(f, header=None, delim_whitespace=True)
if diffractogram.shape[1] == 2:
diffractogram.columns = ['2th', 'I']
elif diffractogram.shape[1] == 3:
diffractogram.columns = ['2th', 'I', 'sigma']
return diffractogram, wavelength
def read_data(data, options={}, index=0):
beamline_extensions = ['mar3450', 'edf', 'cbf']
file_extension = data['path'][index].split('.')[-1]
if file_extension in beamline_extensions:
diffractogram, wavelength = integrate_1d(data=data, options=options, index=index)
elif file_extension == 'brml':
diffractogram, wavelength = read_brml(data=data, options=options, index=index)
elif file_extension in['xy', 'xye']:
diffractogram, wavelength = read_xy(data=data, options=options, index=index)
if options['offset'] or options['normalise']:
# Make copy of the original intensities before any changes are made through normalisation or offset, to easily revert back if need to update.
diffractogram['I_org'] = diffractogram['I']
diffractogram['2th_org'] = diffractogram['2th']
diffractogram = apply_offset(diffractogram, wavelength, index, options)
diffractogram = translate_wavelengths(data=diffractogram, wavelength=wavelength)
return diffractogram, wavelength
def apply_offset(diffractogram, wavelength, index, options):
if 'current_offset_y' not in options.keys():
options['current_offset_y'] = options['offset_y']
else:
if options['current_offset_y'] != options['offset_y']:
options['offset_change'] = True
options['current_offset_y'] = options['offset_y']
options['current_offset_x'] = options['offset_x']
#Apply offset along y-axis
diffractogram['I'] = diffractogram['I_org'] # Reset intensities
if options['normalise']:
diffractogram['I'] = diffractogram['I'] / diffractogram['I'].max()
diffractogram['I'] = diffractogram['I'] + index*options['offset_y']
# Apply offset along x-axis
relative_shift = (wavelength / 1.54059)*options['offset_x'] # Adjusts the offset-factor to account for wavelength, so that offset_x given is given in 2th_cuka-units
diffractogram['2th'] = diffractogram['2th_org']
diffractogram['2th'] = diffractogram['2th'] + index*relative_shift
return diffractogram
def revert_offset(diffractogram,which=None):
if which == 'both':
diffractogram['2th'] = diffractogram['2th_org']
diffractogram['I'] = diffractogram['I_org']
if which == 'y':
diffractogram['I'] = diffractogram['I_org']
if which == 'x':
diffractogram['2th'] = diffractogram['2th_org']
return diffractogram
def load_reflection_table(data: dict, reflections_params: dict, options={}):
required_options = ['ref_wavelength', 'to_wavelength']
default_options = {
'ref_wavelength': 1.54059,
'to_wavelength': None
}
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
# VESTA outputs the file with a header that has a space between the parameter and units - so there is some extra code to rectify the issue
# that ensues from this formatting
reflections = pd.read_csv(reflections_params['path'], delim_whitespace=True)
# Remove the extra column that appears from the headers issue
reflections.drop(reflections.columns[-1], axis=1, inplace=True)
with open(reflections_params['path'], 'r') as f:
line = f.readline()
headers = line.split()
# Delete the fourth element which is '(Å)'
del headers[4]
# Change name of column to avoid using greek letters
headers[7] = '2th'
# Set the new modified headers as the headers of
reflections.columns = headers
reflections = translate_wavelengths(data=reflections, wavelength=options['ref_wavelength'], to_wavelength=options['to_wavelength'])
if 'heatmap' in data.keys():
start_2th, stop_2th = data['diffractogram'][0]['2th'].min(), data['diffractogram'][0]['2th'].max()
len_2th = stop_2th - start_2th
#print(start_2th, stop_2th, len_2th)
start_heatmap, stop_heatmap = 0, data['heatmap'].shape[1]
len_heatmap = stop_heatmap - start_heatmap
#print(start_heatmap, stop_heatmap, len_heatmap)
scale = len_heatmap/len_2th
#print(scale)
#print(stop_2th * scale)
reflections['heatmap'] = (reflections['2th']-start_2th) * scale
return reflections
def translate_wavelengths(data: pd.DataFrame, wavelength: float, to_wavelength=None) -> pd.DataFrame:
# FIXME Somewhere here there is an invalid arcsin-argument. Not sure where.
pd.options.mode.chained_assignment = None
# Translate to CuKalpha
cuka = 1.54059 # Å
if cuka > wavelength:
max_2th_cuka = 2*np.arcsin(wavelength/cuka) * 180/np.pi
else:
max_2th_cuka = data['2th'].max()
data['2th_cuka'] = np.NAN
data['2th_cuka'].loc[data['2th'] <= max_2th_cuka] = 2*np.arcsin(cuka/wavelength * np.sin((data['2th']/2) * np.pi/180)) * 180/np.pi
# Translate to MoKalpha
moka = 0.71073 # Å
if moka > wavelength:
max_2th_moka = 2*np.arcsin(wavelength/moka) * 180/np.pi
else:
max_2th_moka = data['2th'].max()
data['2th_moka'] = np.NAN
data['2th_moka'].loc[data['2th'] <= max_2th_moka] = 2*np.arcsin(moka/wavelength * np.sin((data['2th']/2) * np.pi/180)) * 180/np.pi
# Convert to other parameters
data['d'] = wavelength / (2*np.sin((2*data['2th']*np.pi/180)/2))
data['1/d'] = 1/data['d']
data['q'] = np.abs((4*np.pi/wavelength)*np.sin(data['2th']/2 * np.pi/180))
data['q2'] = data['q']**2
data['q4'] = data['q']**4
if to_wavelength:
if to_wavelength >= cuka:
max_2th = 2*np.arcsin(cuka/to_wavelength) * 180/np.pi
else:
max_2th = data['2th_cuka'].max()
data['2th'] = np.NAN
data['2th'].loc[data['2th_cuka'] <= max_2th] = 2*np.arcsin(to_wavelength/cuka * np.sin((data['2th_cuka']/2) * np.pi/180)) * 180/np.pi
return data
def find_wavelength_from_xy(path):
wavelength_dict = {'Cu': 1.54059, 'Mo': 0.71073}
with open(path, 'r') as f:
lines = f.readlines()
for line in lines:
# For .xy-files output from EVA
if 'Anode' in line:
anode = line.split()[8].strip('"')
wavelength = wavelength_dict[anode]
# For .xy-files output from pyFAI integration
elif 'Wavelength' in line:
wavelength = float(line.split()[2])*10**10
return wavelength

672
nafuma/xrd/plot.py Normal file
View file

@ -0,0 +1,672 @@
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator)
import pandas as pd
import numpy as np
import math
import ipywidgets as widgets
from IPython.display import display
import beamtime.xrd as xrd
import beamtime.auxillary as aux
import beamtime.plotting as btp
def plot_diffractogram(data, options={}):
''' Plots a diffractogram.
Input:
data (dict): Must include path = string to diffractogram data, and plot_kind = (recx, beamline, image)'''
# Update options
required_options = ['x_vals', 'y_vals', 'ylabel', 'xlabel', 'xunit', 'yunit', 'line', 'scatter', 'xlim', 'ylim', 'normalise', 'offset', 'offset_x', 'offset_y', 'offset_change',
'reflections_plot', 'reflections_indices', 'reflections_data', 'heatmap', 'cmap', 'plot_kind', 'palettes', 'interactive', 'rc_params', 'format_params', 'interactive_session_active']
default_options = {
'x_vals': '2th',
'y_vals': 'I',
'ylabel': 'Intensity', 'xlabel': '2theta',
'xunit': 'deg', 'yunit': 'a.u.',
'xlim': None, 'ylim': None,
'normalise': True,
'offset': True,
'offset_x': 0,
'offset_y': 1,
'offset_change': False,
'line': True, # whether or not to plot diffractogram as a line plot
'scatter': False, # whether or not to plot individual data points
'reflections_plot': False, # whether to plot reflections as a plot
'reflections_indices': False, # whether to plot the reflection indices
'reflections_data': None, # Should be passed as a list of dictionaries on the form {path: rel_path, reflection_indices: number of indices, colour: [r,g,b], min_alpha: 0-1]
'heatmap': False,
'cmap': 'viridis',
'plot_kind': None,
'palettes': [('qualitative', 'Dark2_8')],
'interactive': False,
'interactive_session_active': False,
'rc_params': {},
'format_params': {},
}
if 'offset_y' not in options.keys():
if len(data['path']) > 10:
default_options['offset_y'] = 0.05
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
#options['current_offset_y'] = options['offset_y']
# Convert data['path'] to list to allow iteration over this to accommodate both single and multiple diffractograms
if not isinstance(data['path'], list):
data['path'] = [data['path']]
# Check if there is some data stored already, load in data if not. This speeds up replotting in interactive mode.
if not 'diffractogram' in data.keys():
# Initialise empty list for diffractograms and wavelengths
data['diffractogram'] = [None for _ in range(len(data['path']))]
data['wavelength'] = [None for _ in range(len(data['path']))]
for index in range(len(data['path'])):
diffractogram, wavelength = xrd.io.read_data(data=data, options=options, index=index)
data['diffractogram'][index] = diffractogram
data['wavelength'][index] = wavelength
# Sets the xlim if this has not bee specified
if not options['xlim']:
options['xlim'] = [data['diffractogram'][0][options['x_vals']].min(), data['diffractogram'][0][options['x_vals']].max()]
# Generate heatmap data
data['heatmap'], data['heatmap_xticks'], data['heatmap_xticklabels'] = generate_heatmap(data=data, options=options)
options['heatmap_loaded'] = True
if options['heatmap']:
options['xlim'] = options['heatmap_xlim']
else:
if not isinstance(data['diffractogram'], list):
data['diffractogram'] = [data['diffractogram']]
data['wavelength'] = [data['wavelength']]
if options['interactive_session_active']:
if options['offset']:
if (options['offset_x'] != options['current_offset_x']) or (options['offset_y'] != options['current_offset_y']):
for i, (diff, wl) in enumerate(zip(data['diffractogram'], data['wavelength'])):
xrd.io.apply_offset(diff, wl, i, options)
# Start inteactive session with ipywidgets. Disables options['interactive'] in order for the interactive loop to not start another interactive session
if options['interactive']:
options['interactive'] = False
options['interactive_session_active'] = True
plot_diffractogram_interactive(data=data, options=options)
return
# Makes a list out of reflections_data if it only passed as a dict, as it will be looped through later
if options['reflections_data']:
if not isinstance(options['reflections_data'], list):
options['reflections_data'] = [options['reflections_data']]
# Determine number of subplots and height ratios between them
if len(options['reflections_data']) >= 1:
options = determine_grid_layout(options=options)
# Prepare plot, and read and process data
fig, ax = btp.prepare_plot(options=options)
# Assign the correct axes
if options['reflections_plot'] or options['reflections_indices']:
if options['reflections_indices']:
indices_ax = ax[0]
if options['reflections_plot']:
ref_axes = [axx for axx in ax[range(1,len(options['reflections_data'])+1)]]
else:
ref_axes = [axx for axx in ax[range(0,len(options['reflections_data']))]]
ax = ax[-1]
if len(data['path']) < 10:
colours = btp.generate_colours(options['palettes'])
else:
colours = btp.generate_colours(['black'], kind='single')
if options['heatmap']:
sns.heatmap(data['heatmap'], cmap=options['cmap'], cbar=False, ax=ax)
ax.set_xticks(data['heatmap_xticks'][options['x_vals']])
ax.set_xticklabels(data['heatmap_xticklabels'][options['x_vals']])
ax.tick_params(axis='x', which='minor', bottom=False, top=False)
else:
for diffractogram in data['diffractogram']:
if options['line']:
diffractogram.plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=next(colours), zorder=1)
if options['scatter']:
ax.scatter(x=diffractogram[options['x_vals']], y = diffractogram[options['y_vals']], c=[(1,1,1,0)], edgecolors=[next(colours)], linewidths=plt.rcParams['lines.markeredgewidth'], zorder=2) #, edgecolors=np.array([next(colours)]))
fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options)
# Make the reflection plots. By default, the wavelength of the first diffractogram will be used for these.
if options['reflections_plot'] and options['reflections_data']:
options['xlim'] = ax.get_xlim()
options['to_wavelength'] = data['wavelength'][0]
for reflections_params, axis in zip(options['reflections_data'], ref_axes):
plot_reflection_table(data=data, reflections_params=reflections_params, ax=axis, options=options)
# Print the reflection indices. By default, the wavelength of the first diffractogram will be used for this.
if options['reflections_indices'] and options['reflections_data']:
options['xlim'] = ax.get_xlim()
options['to_wavelength'] = data['wavelength'][0]
for reflections_params in options['reflections_data']:
plot_reflection_indices(data=data, reflections_params=reflections_params, ax=indices_ax, options=options)
if options['interactive_session_active']:
options['current_y_offset'] = options['widget'].kwargs['offset_y']
update_widgets(data=data, options=options)
return data['diffractogram'], fig, ax
def generate_heatmap(data, options={}):
required_options = ['x_tick_locators']
default_options = {
'x_tick_locators': [0.5, 0.1]
}
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
twotheta = []
intensities = []
scans = []
for i, d in enumerate(data['diffractogram']):
twotheta = np.append(twotheta, d['2th'].to_numpy())
intensities = np.append(intensities, d['I'].to_numpy())
scans = np.append(scans, np.full(len(d['2th'].to_numpy()), int(i)))
heatmap = pd.DataFrame({'2th': twotheta, 'scan': scans, 'I': intensities})
xrd.io.translate_wavelengths(data=heatmap, wavelength=data['wavelength'][0])
min_dict = {'2th': heatmap['2th'].min(), '2th_cuka': heatmap['2th_cuka'].min(), '2th_moka': heatmap['2th_moka'].min(),
'q': heatmap['q'].min(), 'q2': heatmap['q2'].min(), 'q4': heatmap['q4'].min(), '1/d': heatmap['1/d'].min()}
max_dict = {'2th': heatmap['2th'].max(), '2th_cuka': heatmap['2th_cuka'].max(), '2th_moka': heatmap['2th_moka'].max(),
'q': heatmap['q'].max(), 'q2': heatmap['q2'].max(), 'q4': heatmap['q4'].max(), '1/d': heatmap['1/d'].max()}
ndatapoints = len(data['diffractogram'][0]['2th'])
xlims = [0, ndatapoints, 0, ndatapoints] # 0: xmin, 1: xmax, 2: xmin_start, 3: xmax_start
xticks = {}
xticklabels = {}
for xval in min_dict.keys():
# Add xticks labels
label_max = aux.floor(max_dict[xval], roundto=options['x_tick_locators'][0])
label_min = aux.ceil(min_dict[xval], roundto=options['x_tick_locators'][0])
label_steps = (label_max - label_min)/options['x_tick_locators'][0]
xticklabels[xval] = np.linspace(label_min, label_max, num=int(label_steps)+1)
# Add xticks
xval_span = max_dict[xval] - min_dict[xval]
steps = xval_span / ndatapoints
xticks_xval = []
for tick in xticklabels[xval]:
xticks_xval.append((tick-min_dict[xval])/steps)
xticks[xval] = xticks_xval
options['x_tick_locators'] = None
heatmap = heatmap.reset_index().pivot_table(index='scan', columns='2th', values='I')
options['heatmap_xlim'] = xlims
return heatmap, xticks, xticklabels
# #results = np.transpose(np.vstack([twotheta, scans, intensities]))
def determine_grid_layout(options):
nrows = 1 if not options['reflections_indices'] else 2
if options['reflections_plot']:
for reference in options['reflections_data']:
nrows += 1
options['format_params']['nrows'] = nrows
options['format_params']['grid_ratio_height'] = [1 for i in range(nrows-1)]+[10]
return options
def plot_diffractogram_interactive(data, options):
# Format here is xminmax[0]: xmin, xminmax[1]: xmax, xminmax[2]: xmin_start, xminmax[3]: xmax_start, where "_start" denotes starting value of the slider
xminmax = { '2th': [None, None, None, None], '2th_cuka': [None, None, None, None], '2th_moka': [None, None, None, None],
'd': [None, None, None, None], '1/d': [None, None, None, None],
'q': [None, None, None, None], 'q2': [None, None, None, None], 'q4': [None, None, None, None],
'heatmap': [None, None, None, None], 'start': [None, None, None, None]}
yminmax = { 'diff': [None, None, None, None], 'heatmap': [None, None, None, None], 'start': [None, None, None, None]}
update_xminmax(xminmax=xminmax, data=data, options=options)
update_yminmax(yminmax=yminmax, data=data, options=options)
options['xminmax'], options['yminmax'] = xminmax, yminmax
# Get start values for ylim slider based on choice (FIXME This can be impleneted into update_yminmax). Can also make a 'start' item that stores the start values, instead of having 4 items in 'diff' as it is now.
if options['heatmap']:
ymin = yminmax['heatmap'][0]
ymax = yminmax['heatmap'][1]
ymin_start = yminmax['heatmap'][0]
ymax_start = yminmax['heatmap'][1]
elif not options['heatmap']:
ymin = yminmax['diff'][0]
ymax = yminmax['diff'][1]
ymin_start = yminmax['diff'][2]
ymax_start = yminmax['diff'][3]
# FIXME The start values for xlim should probably also be decided by initial value of x_vals, and can likewise be implemented in update_xminmax()
options['widgets'] = {
'xlim': {
'w': widgets.FloatRangeSlider(value=[xminmax['start'][2], xminmax['start'][3]], min=xminmax['start'][0], max=xminmax['start'][1], step=0.5, layout=widgets.Layout(width='95%')),
'state': options['x_vals'],
'2th_default': {'min': xminmax['2th'][0], 'max': xminmax['2th'][1], 'value': [xminmax['2th'][0], xminmax['2th'][1]], 'step': 0.5},
'2th_cuka_default': {'min': xminmax['2th_cuka'][0], 'max': xminmax['2th_cuka'][1], 'value': [xminmax['2th_cuka'][0], xminmax['2th_cuka'][1]], 'step': 0.5},
'2th_moka_default': {'min': xminmax['2th_moka'][0], 'max': xminmax['2th_moka'][1], 'value': [xminmax['2th_moka'][0], xminmax['2th_moka'][1]], 'step': 0.5},
'd_default': {'min': xminmax['d'][0], 'max': xminmax['d'][1], 'value': [xminmax['d'][0], xminmax['d'][1]], 'step': 0.5},
'1/d_default': {'min': xminmax['1/d'][0], 'max': xminmax['1/d'][1], 'value': [xminmax['1/d'][0], xminmax['1/d'][1]], 'step': 0.5},
'q_default': {'min': xminmax['q'][0], 'max': xminmax['q'][1], 'value': [xminmax['q'][0], xminmax['q'][1]], 'step': 0.5},
'q2_default': {'min': xminmax['q2'][0], 'max': xminmax['q2'][1], 'value': [xminmax['q2'][0], xminmax['q2'][1]], 'step': 0.5},
'q4_default': {'min': xminmax['q4'][0], 'max': xminmax['q4'][1], 'value': [xminmax['q4'][0], xminmax['q4'][1]], 'step': 0.5},
'heatmap_default': {'min': xminmax['heatmap'][0], 'max': xminmax['heatmap'][1], 'value': [xminmax['heatmap'][0], xminmax['heatmap'][1]], 'step': 10}
},
'ylim': {
'w': widgets.FloatRangeSlider(value=[yminmax['start'][2], yminmax['start'][3]], min=yminmax['start'][0], max=yminmax['start'][1], step=0.5, layout=widgets.Layout(width='95%')),
'state': 'heatmap' if options['heatmap'] else 'diff',
'diff_default': {'min': yminmax['diff'][0], 'max': yminmax['diff'][1], 'value': [yminmax['diff'][2], yminmax['diff'][3]], 'step': 0.1},
'heatmap_default': {'min': yminmax['heatmap'][0], 'max': yminmax['heatmap'][1], 'value': [yminmax['heatmap'][0], yminmax['heatmap'][1]], 'step': 0.1}
}
}
if options['reflections_data']:
w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(plot_diffractogram), data=widgets.fixed(data), options=widgets.fixed(options),
scatter=widgets.ToggleButton(value=False),
line=widgets.ToggleButton(value=True),
reflections_plot=widgets.ToggleButton(value=True),
reflections_indices=widgets.ToggleButton(value=False),
heatmap=widgets.ToggleButton(value=options['heatmap']),
x_vals=widgets.Dropdown(options=['2th', 'd', '1/d', 'q', 'q2', 'q4', '2th_cuka', '2th_moka'], value='2th', description='X-values'),
xlim=options['widgets']['xlim']['w'],
ylim=options['widgets']['ylim']['w'],
offset_y=widgets.BoundedFloatText(value=options['offset_y'], min=-5, max=5, step=0.01, description='offset_y'),
offset_x=widgets.BoundedFloatText(value=options['offset_x'], min=-1, max=1, step=0.01, description='offset_x')
)
else:
w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(plot_diffractogram), data=widgets.fixed(data), options=widgets.fixed(options),
scatter=widgets.ToggleButton(value=False),
line=widgets.ToggleButton(value=True),
xlim=options['widgets']['xlim']['w'])
options['widget'] = w
display(w)
def update_xminmax(xminmax, data, options={}):
''' Finds minimum and maximum values of each column and updates the minmax dictionary to contain the correct values.
Input:
minmax (dict): contains '''
xminmax['2th'] = [None, None, None, None]
for index, diffractogram in enumerate(data['diffractogram']):
if not xminmax['2th'][0] or diffractogram['2th'].min() < xminmax['2th'][0]:
xminmax['2th'][0] = diffractogram['2th'].min()
min_index = index
if not xminmax['2th'][1] or diffractogram['2th'].max() > xminmax['2th'][1]:
xminmax['2th'][1] = diffractogram['2th'].max()
max_index = index
xminmax['2th'][2], xminmax['2th'][3] = xminmax['2th'][0], xminmax['2th'][1]
xminmax['2th_cuka'][0], xminmax['2th_cuka'][1] = data['diffractogram'][min_index]['2th_cuka'].min(), data['diffractogram'][max_index]['2th_cuka'].max()
xminmax['2th_cuka'][2], xminmax['2th_cuka'][3] = xminmax['2th_cuka'][0], xminmax['2th_cuka'][1]
xminmax['2th_moka'][0], xminmax['2th_moka'][1] = data['diffractogram'][min_index]['2th_moka'].min(), data['diffractogram'][max_index]['2th_moka'].max()
xminmax['2th_moka'][2], xminmax['2th_moka'][3] = xminmax['2th_moka'][0], xminmax['2th_moka'][1]
xminmax['d'][0], xminmax['d'][1] = data['diffractogram'][max_index]['d'].min(), data['diffractogram'][min_index]['d'].max() # swapped, intended
xminmax['d'][2], xminmax['d'][3] = xminmax['d'][0], xminmax['d'][1]
xminmax['1/d'][0], xminmax['1/d'][1] = data['diffractogram'][min_index]['1/d'].min(), data['diffractogram'][max_index]['1/d'].max()
xminmax['1/d'][2], xminmax['1/d'][3] = xminmax['1/d'][0], xminmax['1/d'][1]
xminmax['q'][0], xminmax['q'][1] = data['diffractogram'][min_index]['q'].min(), data['diffractogram'][max_index]['q'].max()
xminmax['q'][2], xminmax['q'][3] = xminmax['q'][0], xminmax['q'][1]
xminmax['q2'][0], xminmax['q2'][1] = data['diffractogram'][min_index]['q2'].min(), data['diffractogram'][max_index]['q2'].max()
xminmax['q2'][2], xminmax['q2'][3] = xminmax['q2'][0], xminmax['q2'][1]
xminmax['q4'][0], xminmax['q4'][1] = data['diffractogram'][min_index]['q4'].min(), data['diffractogram'][max_index]['q4'].max()
xminmax['q4'][2], xminmax['q4'][3] = xminmax['q4'][0], xminmax['q4'][1]
xminmax['heatmap'] = options['heatmap_xlim'] # This value is set in the generate_heatmap()-function
xminmax['start'][0], xminmax['start'][1] = xminmax[options['x_vals']][0], xminmax[options['x_vals']][1]
xminmax['start'][2], xminmax['start'][3] = xminmax[options['x_vals']][2], xminmax[options['x_vals']][3]
def update_yminmax(yminmax: dict, data: dict, options={}) -> None:
yminmax['diff'] = [None, None, None, None]
# Go through diffractograms and find the minimum and maximum intensity values
for diffractogram in data['diffractogram']:
if not yminmax['diff'][0] or (yminmax['diff'][0] > (diffractogram['I'].min())):
yminmax['diff'][0] = diffractogram['I'].min()
if not yminmax['diff'][1] or (yminmax['diff'][1] < (diffractogram['I'].max())):
yminmax['diff'][1] = diffractogram['I'].max()
# Set start values of ymin and ymax to be slightly below lowest data points and slightly above highest data points to give some whitespace around the plot
yminmax['diff'][2] = yminmax['diff'][0] - 0.1*yminmax['diff'][1]
yminmax['diff'][3] = yminmax['diff'][1] + 0.2*yminmax['diff'][1]
# Allow for adjustment up to five times ymax above and below data
yminmax['diff'][0] = yminmax['diff'][0] - 5*yminmax['diff'][1]
yminmax['diff'][1] = yminmax['diff'][1]*5
# Set start values to the edges of the dataset
yminmax['heatmap'][0], yminmax['heatmap'][1] = 0, data['heatmap'].shape[0]
yminmax['heatmap'][2], yminmax['heatmap'][3] = yminmax['heatmap'][0], yminmax['heatmap'][1]
if options['heatmap']:
yminmax['start'][0], yminmax['start'][1] = yminmax['heatmap'][0], yminmax['heatmap'][1]
yminmax['start'][2], yminmax['start'][3] = yminmax['heatmap'][0], yminmax['heatmap'][1]
else:
# The third and fourth index are different here to not be zoomed completely out to begin with.
yminmax['start'][0], yminmax['start'][1] = yminmax['diff'][0], yminmax['diff'][1]
yminmax['start'][2], yminmax['start'][3] = yminmax['diff'][2], yminmax['diff'][3]
def update_defaults(widget: dict, minmax: dict) -> None:
''' Updates the default x- or y-limits of a given widget. Refer to plot_diffractogram_interactive() to see the form of the widget that is passed in. An update of the min/max-values is done just prior to calling this function.
Changes dictionaries in place.
Input:
widget (dict): A dictionary containing the widget itself (widget['w']) and all its default-values (e.g. widget['2th_default'])
minmax (dict): A dictionary containing min and max values, as well as min_start and max_start values. (e.g. minmax['2th'] is a list with four elements: [xmin, xmax, xmin_start, xmax_start])
Output:
None.'''
for name, attr in widget.items():
if name.endswith('default'):
attr['min'] = minmax[name.replace('_default', '')][0]
attr['max'] = minmax[name.replace('_default', '')][1]
attr['value'] = [minmax[name.replace('_default', '')][2], minmax[name.replace('_default', '')][3]]
def update_widgets(data, options):
for widget_name, widget in options['widgets'].items():
# Make changes to xlim-widget
if widget_name == 'xlim':
# First update the min and max values
update_xminmax(xminmax=options['xminmax'], data=data, options=options)
update_defaults(widget=widget, minmax=options['xminmax'])
if options['heatmap'] and (widget['state'] != 'heatmap'):
setattr(widget['w'], 'min', widget['heatmap_default']['min'])
setattr(widget['w'], 'max', widget['heatmap_default']['max'])
setattr(widget['w'], 'value', widget['heatmap_default']['value'])
setattr(widget['w'], 'step', widget['heatmap_default']['step'])
widget['state'] = 'heatmap'
elif not options['heatmap'] and (widget['state'] != options['x_vals']):
# Then loop through all attributes in the widget and change to current mode.
for arg in widget[f'{options["x_vals"]}_default']:
# If new min value is larger than previous max, or new max value is smaller than previous min, set the opposite first
if arg == 'min':
if widget[f'{options["x_vals"]}_default']['min'] > getattr(widget['w'], 'max'):
setattr(widget['w'], 'max', widget[f'{options["x_vals"]}_default']['max'])
elif arg == 'max':
if widget[f'{options["x_vals"]}_default']['max'] < getattr(widget['w'], 'min'):
setattr(widget['w'], 'min', widget[f'{options["x_vals"]}_default']['min'])
setattr(widget['w'], arg, widget[f'{options["x_vals"]}_default'][arg])
widget['state'] = options['x_vals']
# Make changes to ylim-widget
elif widget_name == 'ylim':
update_yminmax(yminmax=options['yminmax'], data=data, options=options)
update_defaults(widget=widget, minmax=options['yminmax'])
state = 'heatmap' if options['heatmap'] else 'diff'
if widget['state'] != state or options['offset_change']:
for arg in widget[f'{state}_default']:
# If new min value is larger than previous max, or new max value is smaller than previous min, set the opposite first
if arg == 'min':
if widget[f'{state}_default']['min'] > getattr(widget['w'], 'max'):
setattr(widget['w'], 'max', widget[f'{state}_default']['max'])
elif arg == 'max':
if widget[f'{state}_default']['max'] < getattr(widget['w'], 'min'):
setattr(widget['w'], 'min', widget[f'{state}_default']['min'])
setattr(widget['w'], arg, widget[f'{state}_default'][arg])
options['offset_change'] = False
widget['state'] = state
def plot_reflection_indices(data, reflections_params, ax, options={}):
''' Print reflection indices from output generated by VESTA.
Required contents of data:
path (str): relative path to reflection table file'''
required_options = ['reflection_indices', 'text_colour', 'hide_indices']
default_options = {
'reflection_indices': 3, # Number of reflection indices to plot, from highest intensity and working its way down
'text_colour': 'black',
'hide_indices': False
}
reflections_params = aux.update_options(options=reflections_params, required_options=required_options, default_options=default_options)
if not reflections_params['hide_indices']:
reflection_table = xrd.io.load_reflection_table(data=data, reflections_params=reflections_params, options=options)
if reflections_params['reflection_indices'] > 0:
# Get the data['reflection_indices'] number of highest reflections within the subrange options['xlim']
x_vals = 'heatmap' if options['heatmap'] else options['x_vals']
reflection_indices = reflection_table.loc[(reflection_table[x_vals] > options['xlim'][0]) & (reflection_table[x_vals] < options['xlim'][1])].nlargest(options['reflection_indices'], 'I')
# Plot the indices
for i in range(reflections_params['reflection_indices']):
if reflection_indices.shape[0] > i:
ax.text(s=f'({reflection_indices["h"].iloc[i]} {reflection_indices["k"].iloc[i]} {reflection_indices["l"].iloc[i]})', x=reflection_indices[x_vals].iloc[i], y=0, fontsize=2.5, rotation=90, va='bottom', ha='center', c=reflections_params['text_colour'])
if options['xlim']:
ax.set_xlim(options['xlim'])
ax.axis('off')
return
def plot_reflection_table(data, reflections_params, ax=None, options={}):
''' Plots a reflection table from output generated by VESTA.
Required contents of data:
path (str): relative path to reflection table file'''
required_options = ['reflection_indices', 'reflections_colour', 'min_alpha', 'wavelength', 'format_params', 'rc_params', 'label']
default_options = {
'reflection_indices': 0, # Number of indices to print
'reflections_colour': [0,0,0],
'min_alpha': 0,
'wavelength': 1.54059, # CuKalpha, [Å]
'format_params': {},
'rc_params': {},
'label': None
}
if 'colour' in data.keys():
options['reflections_colour'] = reflections_params['colour']
if 'min_alpha' in reflections_params.keys():
options['min_alpha'] = reflections_params['min_alpha']
if 'reflection_indices' in reflections_params.keys():
options['reflection_indices'] = reflections_params['reflection_indices']
if 'label' in reflections_params.keys():
options['label'] = reflections_params['label']
if 'wavelength' in reflections_params.keys():
options['wavelength'] = reflections_params['wavelength']
options = aux.update_options(options=options, required_options=required_options, default_options=default_options)
if not ax:
_, ax = btp.prepare_plot(options)
x_vals = 'heatmap' if options['heatmap'] else options['x_vals']
reflection_table = xrd.io.load_reflection_table(data=data, reflections_params=reflections_params, options=options)
reflections, intensities = reflection_table[x_vals], reflection_table['I']
colours = []
for ref, intensity in zip(reflections, intensities):
colour = list(options['reflections_colour'])
rel_intensity = (intensity / intensities.max())*(1-options['min_alpha']) + options['min_alpha']
colour.append(rel_intensity)
colours.append(colour)
ax.vlines(x=reflections, ymin=-1, ymax=1, colors=colours, lw=0.5)
ax.set_ylim([-0.5,0.5])
ax.tick_params(which='both', bottom=False, labelbottom=False, right=False, labelright=False, left=False, labelleft=False, top=False, labeltop=False)
if options['xlim']:
ax.set_xlim(options['xlim'])
if options['label']:
xlim_range = ax.get_xlim()[1] - ax.get_xlim()[0]
ylim_avg = (ax.get_ylim()[0]+ax.get_ylim()[1])/2
ax.text(s=reflections_params['label'], x=(ax.get_xlim()[0]-0.01*xlim_range), y=ylim_avg, ha = 'right', va = 'center')
def prettify_labels(label):
labels_dict = {
'2th': '2$\\theta$',
'I': 'Intensity'
}
return labels_dict[label]
def reverse_diffractograms(diffractograms):
rev_diffractograms = []
for i in len(diffractograms):
rev_diffractograms.append(diffractograms.pop())
return rev_diffractograms
#def plot_heatmap():

0
nafuma/xrd/test.txt Normal file
View file