Rename package to nafuma from beamtime
This commit is contained in:
parent
049f30d96b
commit
27c911cf54
25 changed files with 5 additions and 7 deletions
1
nafuma/__init__.py
Normal file
1
nafuma/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
55
nafuma/auxillary.py
Normal file
55
nafuma/auxillary.py
Normal 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
|
||||
1
nafuma/electrochemistry/__init__.py
Normal file
1
nafuma/electrochemistry/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import io, plot, unit_tables
|
||||
582
nafuma/electrochemistry/io.py
Normal file
582
nafuma/electrochemistry/io.py
Normal 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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
387
nafuma/electrochemistry/plot.py
Normal file
387
nafuma/electrochemistry/plot.py
Normal 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
|
||||
53
nafuma/electrochemistry/unit_tables.py
Normal file
53
nafuma/electrochemistry/unit_tables.py
Normal 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
0
nafuma/pdf/__init__.py
Normal file
342
nafuma/plotting.py
Normal file
342
nafuma/plotting.py
Normal 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
1
nafuma/test.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
sdfsdfsdfsdf
|
||||
0
nafuma/test/__init__.py
Normal file
0
nafuma/test/__init__.py
Normal file
9
nafuma/test/pytest.ini
Normal file
9
nafuma/test/pytest.ini
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# pytest.ini
|
||||
|
||||
[pytest]
|
||||
minversion = 6.0
|
||||
testpaths =
|
||||
.
|
||||
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
78
nafuma/test/test_auxillary.py
Normal file
78
nafuma/test/test_auxillary.py
Normal 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)
|
||||
181
nafuma/test/test_plotting.py
Normal file
181
nafuma/test/test_plotting.py
Normal 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)
|
||||
|
||||
0
nafuma/test/xrd/test_io.py
Normal file
0
nafuma/test/xrd/test_io.py
Normal file
0
nafuma/test/xrd/test_plot.py
Normal file
0
nafuma/test/xrd/test_plot.py
Normal file
0
nafuma/test2.txt
Normal file
0
nafuma/test2.txt
Normal file
1
nafuma/xanes/__init__.py
Normal file
1
nafuma/xanes/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import io, calib
|
||||
94
nafuma/xanes/calib.py
Normal file
94
nafuma/xanes/calib.py
Normal 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
2
nafuma/xanes/io.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
#hello
|
||||
#yeah
|
||||
1
nafuma/xrd/__init__.py
Normal file
1
nafuma/xrd/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import io, plot
|
||||
533
nafuma/xrd/io.py
Normal file
533
nafuma/xrd/io.py
Normal 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
672
nafuma/xrd/plot.py
Normal 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
0
nafuma/xrd/test.txt
Normal file
Loading…
Add table
Add a link
Reference in a new issue