# -*- coding: utf-8 -*-
version = "7.4.13"
Copyright = '@Jens Uhlig'
if 1: #Hide imports
import os
from os import walk
import sys
import pandas
import numpy as np
from numpy import power, log10, shape
import numbers
import matplotlib
import matplotlib.colors as colors
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import matplotlib.image as mpimg
from matplotlib.gridspec import GridSpec
from matplotlib.ticker import FuncFormatter
from matplotlib.colors import BoundaryNorm,Normalize,SymLogNorm
from matplotlib.patches import Rectangle
from matplotlib.ticker import MaxNLocator,LinearLocator
from matplotlib.offsetbox import AnchoredText
from matplotlib.ticker import AutoMinorLocator
from matplotlib.patches import Rectangle
from matplotlib import transforms
import re
import scipy
import scipy.constants
import scipy.interpolate as inp
from scipy.signal import savgol_filter
from scipy.signal import decimate
from scipy.special import erf
from scipy.optimize import minimize
from scipy.stats import binned_statistic
import scipy.stats
import pathlib
from pathlib import Path
from tkinter import filedialog
import tkinter
import time as tm #sorry i use time in my code
import lmfit
import h5py
try:
import keyboard
except:
print('the keyboard module was not imported. on Windows this allows to stop the fit by pressing q/n you can install it with "pip install keyboard" ')
try:
import PyQt5
print('Qt was found consider switching to qt mode with %matplotlib qt (more comfortable)')
except:
try:
import PyQt4
print('Qt was found consider switching to qt mode with %matplotlib qt (more comfortable)')
except:
try:
import qt
print('Qt was found consider switching to qt mode with %matplotlib qt (more comfortable)')
except:
print('Qt was not found')
try:
from pptx import Presentation
from pptx.util import Inches
except:
print('We need python-pptx to create a powerpoint file. Not essential. Either use pip or for anaconda: conda install -c conda-forge python-pptx')
try:
import urllib3
import shutil
except:
print('We need the packages urllib3 and shutil to download files from the web')
plt.ion()
pandas.options.mode.chained_assignment = None # I use this a lot and think I can ignore it
FWHM = 2.35482
shading = 'auto' # gouraud
standard_map = cm.jet
print('Plot_func version %s\nwas imported from path:\n %s' % (version, os.path.dirname(os.path.realpath(__file__))))
print('The current working folder is:\n %s' % os.getcwd())
#use this to trigger a real error for DeprecationWarnings
#np.warnings.filterwarnings('error', category=np.VisibleDeprecationWarning)
def download_notebooks():
'''function loads the workflow notebooks into the active folder'''
http = urllib3.PoolManager()
list_of_tools=['TA_Advanced_Fit.ipynb',
'TA_comparative_plotting_and_data_extraction.ipynb',
'TA_Raw_plotting.ipynb',
'TA_Raw_plotting_and_Simple_Fit.ipynb',
'TA_single_scan_handling.ipynb',
'Function_library_overview.pdf',
'function_library.py',
'import_library.py']
print('Now downloading the workflow tools')
for f in list_of_tools:
url = "https://raw.githubusercontent.com/erdzeichen/KiMoPack/main/Workflow_tools/%s"%f
print('Downloading Workflow Tools/%s'%f)
with open(check_folder(path = 'Workflow_tools', current_path = os.getcwd(), filename = f), 'wb') as out:
r = http.request('GET', url, preload_content=False)
shutil.copyfileobj(r, out)
def download_all():
''' function loads workflow notebooks and example files and tutorials'''
download_notebooks()
http = urllib3.PoolManager()
list_of_tools=['TA_Advanced_Fit.ipynb',
'TA_comparative_plotting_and_data_extraction.ipynb',
'TA_Raw_plotting.ipynb',
'TA_Raw_plotting_and_Simple_Fit.ipynb',
'TA_single_scan_handling.ipynb',
'Function_library_overview.pdf',
'function_library.py',
'import_library.py']
print('Now downloading the workflow tools and tutorials')
list_of_example_data=['sample_1_chirp.dat',
'Sample_2_chirp.dat',
'sample_1.hdf5',
'sample_2.hdf5',
'Sample_1.SIA',
'Sample_2.SIA']
print('Now downloading the example files')
for f in list_of_example_data:
url = "https://raw.githubusercontent.com/erdzeichen/KiMoPack/main/Workflow_tools/Data/%s"%f
print('Downloading Workflow Tools/Data/%s'%f)
with open(check_folder(path = 'Workflow_tools'+os.sep+'Data', current_path = os.getcwd(), filename = f), 'wb') as out:
r = http.request('GET', url, preload_content=False)
shutil.copyfileobj(r, out)
print('Now downloading zipfile with tutorials')
url = "https://raw.githubusercontent.com/erdzeichen/KiMoPack/main/Workflow_tools/Tutorial_Notebooks_for_local_use.zip"
with open(check_folder(path = 'Tutorials', current_path = os.getcwd(), filename = "Tutorial_Notebooks_for_local_use.zip"), 'wb') as out:
r = http.request('GET', url, preload_content=False)
shutil.copyfileobj(r, out)
[docs]def changefonts(weight='bold', font='standard', SMALL_SIZE=11, MEDIUM_SIZE=13, LARGE_SIZE=18):
'''
Small function that sets the matplotlib font sizes and fonts, written as conveniens to not need to remember all the
codes and what is names what. Calling the function will change the matplotlib *rc* settings
Parameters
------------
weight : str, optional
'bold' or 'normal'
font : str, optional
this is a meta switch that changes the family. known are:
'standard'='DejaVu Sans'\n
'arial'='Arial'\n
'helvetica'= 'Helvetica'\n
'garamond'='Garamond'\n
'verdana'='Verdana'\n
'bookman'='Bookman'\n
'times'='Times New Roman'
SMALL_SIZE : int, optional
(DEFAULT = 11)\n
all written text, legend title and face size
MEDIUM_SIZE : int, optional
(DEFAULT = 13)\n
tick size and tick numbers
LARGE_SIZE : int, optional
(DEFAULT = 18)\n
axis titles, figure titles, axis labels
'''
font_dict = {
'standard': {'weight': weight, 'size': SMALL_SIZE, 'family': 'DejaVu Sans'},
'arial': {'weight': weight, 'size': SMALL_SIZE, 'family': 'Arial'},
'helvetica': {'weight': weight, 'size': SMALL_SIZE, 'family': 'Helvetica'},
'garamond': {'weight': weight, 'size': SMALL_SIZE, 'family': 'Garamond'},
'verdana': {'weight': weight, 'size': SMALL_SIZE, 'family': 'Verdana'},
'bookman': {'weight': weight, 'size': SMALL_SIZE, 'family': 'Bookman'},
'times': {'weight': weight, 'size': SMALL_SIZE, 'family': 'Times New Roman'},
}
plt.rc('font', **font_dict[font])
plt.rc('axes', titlesize=LARGE_SIZE, labelweight=weight) # fontsize of the axes title
plt.rc('axes', labelsize=LARGE_SIZE, labelweight=weight) # fontsize of the x and y labels
plt.rc('axes', linewidth=1) # linewidth of all axes
plt.rc('axes', facecolor=(1, 1, 1, 0))
plt.rc('xtick', labelsize=MEDIUM_SIZE) # fontsize of the tick labels
plt.rc('ytick', labelsize=MEDIUM_SIZE) # fontsize of the tick labels
plt.rc('legend', fontsize=SMALL_SIZE) # legend fontsize
plt.rc('legend', title_fontsize=SMALL_SIZE)
plt.rc('legend', facecolor=(1, 1, 1, 0))
plt.rc('legend', edgecolor=(1, 1, 1, 0))
plt.rc('legend', framealpha=0)
plt.rc('figure', titlesize=LARGE_SIZE) # fontsize of the figure title
plt.rc('figure', facecolor=(1, 1, 1, 0)) # fontsize of the figure title
changefonts() #we need to apply the font settings
def clean_double_string(filename, path=None):
'''Stupid function that reads and changes!!! the file. It searchers for double lines and double dots and replaces them with single'''
import re
if path is None: path = os.path.dirname(os.path.realpath(__file__))
with open(Path(os.sep.join([path, filename])), 'r+') as f:
text = f.read()
text = re.sub('--', '-', text)
text = re.sub(r'\.+', '.', text)
f.seek(0)
f.write(text)
f.truncate()
def mouse_move(event):
x, y = event.xdata, event.ydata
print(x, y)
def flatten(mainlist):
return [entry for sublist in mainlist for entry in sublist]
def nearest_neighbor_method3(X, q):
'''returns nearest neighbour value to q'''
X = X.T
return np.argmin(np.sum((X - q) ** 2, axis=1))
def log_and(x, y, *args):
"""Returns the logical and of all 2+ arguments."""
result = np.logical_and(x, y)
for a in args:
result = np.logical_and(result, a)
return result
[docs]def s2_vs_smin2(Spectral_points = 512, Time_points = 130, number_of_species = 3, fitted_kinetic_pars = 7, target_quality = 0.95):
'''dfn is numerator and number of fitted parameters, dfd is denominator and number of degrees of freedom,
F-test is deciding if a set of parameters gives a statistical significant difference. T-test is if a single parameter gives statistical difference.
Null hypothesis, all parameter are zero, if significant, the coefficients improve the fit
the f-statistics compares the number of
"fitted parameter"=number of species*number of spectral points + number of kinetic parameter
"free points"=number of species*number of spectral points*number of time points - fitted parameter
within the target quality, meaning, what fraction do my variances need to have, so that I'm 100% * target_quality sure that they are different from zero'''
data_points = Spectral_points*Time_points
fitted_parameter = Spectral_points*number_of_species+fitted_kinetic_pars
Free_points = data_points-fitted_parameter
f_stat = scipy.stats.f.ppf(q = target_quality, dfn = fitted_parameter, dfd = Free_points)
#print('fitted points:%g\n Free points:%g\n f-stats: %g'%(fitted_parameter,Free_points,f_stat))
return 1+(fitted_parameter*f_stat/Free_points)
[docs]def GUI_open(project_list = None, path = None, filename_part = None, fileending = 'hdf5', sep = "\t", decimal = '.',
index_is_energy = False, transpose = False, sort_indexes = False, divide_times_by = None,
shift_times_by = None, external_time = None, external_wave = None, use_same_name = True, data_type = None,
units = None, baseunit = None, conversion_function = None):
''' This Function
1. opens a gui and allows the selection of multiple saved projects, which are returned as a list
2. if given a list of project names opens them
3. if given the word 'all', opens all files in a given folder
The general behavior is selected by the first parameter (project_list)
it is designed to open combined files that contain both the wavelength and the time. (e.g. SIA files as recorded by Pascher instruments software) or hdf5 projects saved by this software
There are however a lot of additional options to open other ascii type files and adapt their format internally
Important, as default the parameter "fileending" selects hdf5 files only, which are used as project files (see :meth:`plot_func.TA.Save_project`)
for opening of other files the fileending parameter needs to be changed.
Parameters
----------
project_list : list (of str) or 'all', optional
Give a list of filenames that will be opened and returned as a list of objects
if the project list is 'all' then all files in the folder specified in path. The parameter "filename_part"
and "fileending" can be used to specify this selection
path : str or path object (optional)
if path is a string without the operation system dependent separator, it is treated as a relative path,
e.g. data will look from the working directory in the sub director data. Otherwise this has to be a
full path in either strong or path object form.
filename_part : str, optional
This parameter is only used for the option 'all', the (Default) None means do nothing. if a string is given then only
files that start with this string will be read.
fileending : str, optional
this string is used to select the filetype that is suppose to open. For the GUI, only these files will be shown,
with the option 'all' this selects the files that will be read in the folder, 'hdf5' (Default)
sep : str (optional)
is the separator between different numbers, typical are tap '\t' (Default) ,one or
multiple white spaces '\s+' or comma ','.
decimal : str (optional)
sets the ascii symbol that is used for the decimal sign. In most countries this is '.'(Default)
but it can be ',' in countries like Sweden or Germany
index_is_energy : bool (optional)
switches if the wavelength is given in nm (Default) or in eV (if True), currently everything
is handled as wavelength in nm internally
transpose : bool (optional)
if this switch is False (Default) the wavelength are the columns and the rows the times.
data_type: str (optional)
data_type is the string that represents the intensity measurements. Usually this contains if absolute
of differential data. This is used for the color intensity in the 2d plots and the y-axis for the 1d plots
units: str (optional)
this is used to identify the units on the energy axis and to label the slices, recognized is 'nm', 'eV' and 'keV'
but if another unit like 'cm^-1' is used it will state energy in 'cm^-1'. Pleas observe that if you use the index_is_energy
switch the program tries to convert this energy into wavelength.
baseunit: str (optional)
this is used to identify the units on the developing/time axis. This is name that is attached to the index of the dataframe.
setting this during import is equivalent to ta.baseunit
sort_indexes : bool (optional)
For False (Default) I assume that the times and energies are already in a rising order.
with this switch, both are sorted again.
divide_times_by : None or float (optional)
here a number can be given that scales the time by an arbitary factor. This is actually dividing
the times by this value. Alternatively there is the variable self.baseunit. The latter only affects
what is written on the axis, while this value is actually used to scale the times. None (Default)
ignores this
shift_times_by : None, float (optional)
This a value by which the time axis is shifted during import. This is a useful option of e.g.
the recording software does not compensate for t0 and the data is always shifted.
None (Default) ignores this setting
external_time : None or str (optional)
Here a filename extension (string) can be given that contains the time vector.
The file is assumed to be at the same path as the data and to contain a single
type of separated data without header.
If use_same_name = True (default)
It assumes that this is the ending for the file. The filename itself is taken from the filename.
e.g. if samp1.txt is the filename and external_time='.tid' the program searches
samp1.tid for the times. The transpose setting is applied and sets where the times are
to be inserted (row or column indexes)
If use_same_name = False this should be the file containing the vector for the time (in the same format as the main file)
external_wave : None or str (optional)
Here a filename extension (string) can be given that contains the wavelength vector.
If use_same_name = True (default)
The file is assumed to be at the same path as the data and to contain a single type
of separated data without header. This is the ending for the file. The filename itself
is taken from the filename. e.g. if samp1.txt is the filename and external_wave='.wav'
then the program searches samp1.wav for the wavelength. The transpose setting is applied
and sets where the wavelength are to be inserted (columns or row indexes)
If use_same_name = False
this should be a full filename that contains the vector
use_same_name : bool, optional
this switches if the external filename included the loaded filename or is a separate file True(default)
conversion_function: function(optional)
function that receives should have the shape:
return pandas Dataframe with time/frames in rows and wavelength/energy in columns,
The function is tested to accept (in that order) a
my_function(filename, external_time,external_wave),
my_function(filename, external_time),
my_function(filename,external_wave),
my_function(filename) and
return: the dataframe ds with the time_axis as rows and spectral axis as columns
if the ds.index.name ia not empty the "time axis" is in to that name the spectral axis is in ds.columns.name
the return is investigated if it is one, two, or three things.
if two are returned then the second must be the name of what the intensity axis is. This value will then be set to data_type
if three are returned the third is the baseunit (for the time axis) this allows to use the automatic naming in ps or nanosecond
If the values units, data_type or baseunit are (manually) set in the import function the corresponding entries in
datafram will be overwritten
shift_times_by and divide_times_by will be applied if not None (useful to adjust for offset before chirp correction)
Returns
--------------
List of opened TA objects
Examples
--------------
>>> import plot_func as pf
>>> project_list=pf.GUI_open() #start the GUI to open project Files
>>> project_list=pf.GUI_open(fileending='SIA') #start the GUI to open SIA Files
Opening a list of files using the file names
>>> project_list=pf.GUI_open(project_list = ['file1.SIA', 'file2.SIA'])
Opening all files in the folder "all_data" (relative to where the notebook is with the ending "hdf5"
>>> project_list=pf.GUI_open('all',path="all_data")
Opening a list of files with external time vector (of the same name) so it looks for a data
file "file1.txt" and a file with the time information "file1.tid"
>>> project_list=pf.GUI_open(project_list = ['file1.txt', 'file2.txt'], external_time = 'tid')
'''
if project_list is None:
root_window = tkinter.Tk()
root_window.withdraw()
root_window.attributes('-topmost',True)
root_window.after(1000, lambda: root_window.focus_force())
path_list = filedialog.askopenfilename(initialdir=os.getcwd(),multiple=True,filetypes=[('TA project files','*.%s'%fileending)])
if project_list is None:
project_list=[]
elif project_list=='all':
scan_path=check_folder(path = path, current_path = os.getcwd())
if filename_part is not None:#we specified a specific name and want only the files with this name in it
path_list = sorted([os.path.join(scan_path, name) for name in os.listdir(scan_path) if
name.endswith(fileending) and filename_part in name])
else:#we have not specified a specific name and want all files in the folder
path_list = sorted([currentFile for currentFile in scan_path.glob("*.%s"%fileending)])
else:
if len(project_list)<1:
raise ValueError('The use_gui switch is ment to bypass the gui, but you still need at least some files as a list')
else:
if isinstance(project_list, str):project_list=[project_list]
if not hasattr(project_list, '__iter__'):project_list=[project_list]
path_list = []
for filename in project_list:
ta=check_folder(path=path, filename=filename, current_path=os.getcwd())
path_list.append(ta)
return_list = []
for entrance in path_list:
try:
listen=os.path.split(entrance)
path=os.path.normpath(listen[0])
filename=listen[1]
ta = TA(filename = filename, path = path, sep = sep, decimal = decimal,
index_is_energy = index_is_energy, transpose = transpose, sort_indexes = sort_indexes,
divide_times_by = divide_times_by, shift_times_by = shift_times_by, external_time = external_time,
external_wave = external_wave, use_same_name = use_same_name, data_type = data_type, units = units,
baseunit = baseunit, conversion_function = conversion_function)
return_list.append(ta)
except:
print('Problem with entrance:\n %s'%entrance)
return return_list
def check_folder(path = None, current_path = None, filename = None):
'''Helper function using robust path determination.\n
In any case if a valif file name is given it is attached to the total path\n
The path can be string or windows/linux path or pure path or byte type paths.\n
paths that do not exists (including parents) are created\n
1. if path is given absolute, it is returned\n_colors
2. if path is a string (relative) the current_path + path is returned.\n
3. if current_path is not absolute or None, the current working directory is assumed as path.\n
4. IF all is None, the current working directory is returned
Parameters
-----------
path : str, purePath, absolute or relative, optional
the final part of the path used
current_path : None, str, purePath, absolute, optional
path that sits before the "path variable, is filled with current working directory if left None
filename: None, str, optional
attached after path and returned if not None
'''
if isinstance(path,bytes):
path = '%s'%path
if path is not None:
path = pathlib.Path(path)
if isinstance(current_path, bytes):
current_path = '%s'%current_path
if current_path is not None:
current_path=pathlib.Path(current_path)
if isinstance(filename, bytes):
filename='%s'%filename
if filename is not None:
filename = pathlib.Path(filename)
if path is None:
if current_path is None:
directory = Path.cwd()
elif current_path.is_absolute():
directory=current_path
else:
print('attention, current_path was given but not absolute, replaced by cwd')
directory = Path.cwd()
elif path.is_absolute():
directory = path
else:
if current_path is None:
directory = Path.cwd().joinpath(path)
elif current_path.is_absolute():
directory = current_path.joinpath(path)
else:
print('attention, current_path was given but not absolute, replaced by cwd')
directory = Path.cwd().joinpath(path)
directory.mkdir( parents=True, exist_ok=True)
if filename is None:
return directory
else:
return directory.joinpath(filename)
def rebin(ori_df,new_x):
'''interpolation of values to new index'''
if isinstance(ori_df,pandas.DataFrame):
dum={'dummy':new_x}
new_df=pandas.DataFrame(dum,index=new_x)
for col in ori_df.columns:
new_df[col]=np.interp(new_x,ori_df.index.values.astype('float'),ori_df[col].values)
new_df=new_df.drop(['dummy'],axis=1)
return new_df
elif isinstance(ori_df,pandas.Series):
new_df=np.interp(new_x,ori_df.index.values.astype('float'),ori_df.values)
return pandas.Series(new_df,index=new_x)
def savitzky_golay(y, window_size, order, deriv=0, rate=1):
'''Ported from a previous function'''
return savgol_filter(x=y, window_length=window_size, polyorder=order, deriv=deriv, delta=rate)
[docs]def Frame_golay(df, window=5, order=2,transpose=False):
'''Convenience method that returns the Golay smoothed data for each column (DataFrame) or the series
Parameters
-----------
df : pandas.DataFrame,pandas.Series
the DataFrame that has to be interpolated
window_size : int,optional
5(Default) an integer that indicates how many units are to be interpolated
order : int, optional
2 (Default) an integer that indicates what orderpolynoninal is to be used to interpolate the points.
order=1 effectively turns this into a floating average
transpose : bool,optional
in which orientation is the interpolation to be done. Default is in within the column (usually timepoints)
Returns
---------
pandas.DataFrame or pandas.Series
DataFrame or Series with the interpolation applied
'''
#df=df.fillna(0)
if transpose:
df=df.T
if isinstance(df,pandas.DataFrame):
for col in df.columns:
try:
df.loc[:,col]=savitzky_golay(df.loc[:,col].values, window, order)
except:
print(col)
print('was not smoothed')
if transpose:
df=df.T
return df
elif isinstance(df,pandas.Series):
return pandas.Series(savitzky_golay(df.values, window, order),index=df.index)
else:
raise TypeError('must be series or DataFrame')
def find_nearest(arr,value,con_str=False):
'''returns the value in the array closest to value'''
return arr[find_nearest_index(arr,value,con_str=False)]
def find_nearest_index(arr,value,con_str=False):
'''returns the index in the array closest to value (the first one'''
if con_str:
temp_array=np.array(arr,dtype='float')
idx = (np.abs(temp_array-value)).argmin()
else:
idx = (np.abs(arr-value)).argmin()
return idx
def rise(x,sigma=0.1,begin=0):
''' my own implementation of the instrument response function.
Based upon an error function from 0 to 1.
Sigma is the width (after which it has 50%)
and begin is 10% of height'''
return (erf((x-sigma)*np.sqrt(2)/(sigma))+1)/2
def gauss(t,sigma=0.1,mu=0):
'''Gauss function'''
y=np.exp(-0.5*((t-mu)**2)/sigma**2)
y/=sigma*np.sqrt(2*np.pi)
return y
[docs]def norm(df):
'''Min max norming of a dataframe'''
return df.apply(lambda x: (x - np.min(x)) / (np.max(x) - np.min(x)))
def shift(df,name = None,shift = None):
'''Shifts a dataframe along the columns, interpolate and then resample'''
if name is None:name = df.columns
if isinstance(name,type('hello')):name = [name]
for nam in name:
ori_dat = df[nam].values
ori_en = np.array(df.index,dtype = 'float')
if ori_en[0]>ori_en[1]:#oh we have inverse order
dat = np.interp(ori_en[::-1],ori_en[::-1]+shift,ori_dat[::-1])
dat = dat[::-1]
else:
dat = np.interp(ori_en,ori_en+shift,ori_dat)
df[nam] = dat
return df
def colm(k,cmap = standard_map):
'''If a colour map is given, this is used.'''
if isinstance(cmap,type(cm.jet)) or isinstance(cmap,type(cm.viridis)):
if hasattr(k,'__iter__'):
if min(k) >0:#we got a color offset
mini = min(k)/(min(k)+1)
else:
mini = 0
out = [ cmap(x) for x in np.linspace(mini, 1, len(k)+1) ]
out = out[:-1]
return out
else:# get me 10 colors
out = [cmap(x) for x in np.linspace(0, 1, 10)]
ret = out[k]
return ret
else: #we assume it is a iterable thingy
if not hasattr(k,'__iter__'):k = [k]
if isinstance(cmap,pandas.DataFrame):
out = [list(cmap.iloc[ent,:].values) for ent in k]
elif isinstance(cmap,np.ndarray):
out = [cmap[int(ent),:] for ent in k]
elif isinstance(cmap,list):
out = [cmap[int(ent)] for ent in k]
else:
print('didn\'t find the right ')
return out
[docs]def Summarize_scans(list_of_scans = None, path_to_scans = 'Scans', list_to_dump = 'range', window1 = None, window2 = None,
save_name = 'combined.SIA', fileending = 'SIA', filename_part = 'Scan', return_removed_list = False,
sep = "\t", decimal = '.', index_is_energy = False, transpose = False, sort_indexes = False,
divide_times_by = None, shift_times_by = None, external_time = None, external_wave = None, use_same_name = True,
return_ds_only=False, data_type = None, units = None, baseunit = None, conversion_function = None, fitcoeff = None,
base_TA_object = None, value_filter = None, zscore_filter_level = None, zscore_in_window = True,
dump_times = True, replace_values = None, drop_scans = False):
'''
Average single scans. Uses single scans of the data set and plots them as average after different conditions. Usually one defines one or two windows in which the intensity is integrated. This integrated number is then displayed for each scan in the list. There are different tools to select certain scans that are excluded from the summary. These are defined in the list_to_dump. This list can take either be a list with the number, or a string with the words 'single' or 'range' (see below)
Parameters
-----------
list_of_scans : None, 'gui' or list
'gui' (choose scans via gui)\n
None (Default) load scan files from the specified folder (path_to_scans) with the specified file-ending
(file_ending), if filename_part is a string than only files with this string in the name are taken\n
list of names (strings) loads this list of files
list of integers (that will be directly attached to the filename_part) to form the file name
path_to_scans : None, str or path object, optional
specify relative or absolute path to the scan-files (Default:'Scans')
file_ending : str, optional
specify the file extension of the single scan files. The Gui will only show this fileending
(Default: '.SIA')
filename_part : str
specify a part of the string included in all scan-files (Default: 'Scan')
window1: None or list of 4 floats, optional
window in time and wavelength over which each scan is averaged.\n
window must have the shape [start time, end time, start wavelength, end wavelength]
(Default: None)
window2: list of 4 floats, optional
window in time and wavelength over which each scan is averaged.\n
window must have the shape [start time, end time, start wavelength, end wavelength]
(Default: None) IF not given then only one window will be displayed
list_to_dump : list, 'single' or 'range', or None, optional
takes a list of scans to be excluded from the average, this list can be indexes (order)
in which the scans come, or a list of names. if this is given as a list the option "range"
is offered, which allows to add additional selection to the cut.\n
**'single'** allows you (in a GUI) to click on single points in plotted window1 or two that
is to be removed, useful for spike removal and makes only sense in conjunction with at least
a defined window1, if none is defined window1 = [0.5,10,300,1200] will be set automatically.
A right click removes the last selection a middle click applies it. An empty middle click
(without selecting anything) finishes the gui\n
**'range'** allows you (in a GUI) to click and define regions.\n
first left click is the left side of the window, second left click the ride side of the window.
Third left click the left side of the second window,... A right click removes the last set point.
a middle click finishes and applies the selection\n
An **empty middle click** (without selecting anything) finishes the gui\n
useful for spike removal and definition of exclusion region (e.g. where the sample died)
This makes only sense in conjunction with at least a defined window1 ,
if none is defined window1 = [0.5,10,300,1200] will be set automatically
if None then it is not filtered, but simply returned
data_type: str (optional)
data_type is the string that represents the intensity measurements. Usually this contains if absolute
of differential data. This is used for the color intensity in the 2d plots and the y-axis for the 1d plots
units: str (optional)
this is used to identify the units on the energy axis and to label the slices, recognized is 'nm', 'eV' and 'keV'
but if another unit like 'cm^-1' is used it will state energy in 'cm^-1'. Pleas observe that if you use the index_is_energy
switch the program tries to convert this energy into wavelength.
baseunit: str (optional)
this is used to identify the units on the developing/time axis. This is name that is attached to the index of the dataframe.
setting this during import is equivalent to ta.baseunit
save_name : str, optional
specify name for saving the combined scans (Default) 'combined.SIA')
return_removed_list : bool, optional
(Default) False, returns the list of removed scans instead of the averaged data set. (this list could then be given as "list_to_dump" to get the averaged datafile too. If a file name is given for saved file (which is Default) then the file is saved anyways.
sep : str (optional)
is the separator between different numbers, typical are tap (Backslash t) (Default) ,one or
multiple white spaces 'backslash s+' or comma ','.
decimal : str (optional)
sets the ascii symbol that is used for the decimal sign. In most countries this is '.'(Default)
but it can be ',' in countries like Sweden or Germany
index_is_energy : bool (optional)
switches if the wavelength is given in nm (Default) or in eV (if True), currently everything
is handled as wavelength in nm internally
transpose : bool (optional)
if this switch is False (Default) the wavelength are the columns and the rows the times.
sort_indexes : bool (optional)
For False (Default) I assume that the times and energies are already in a rising order.
with this switch, both are sorted again.
divide_times_by : None or float (optional)
here a number can be given that scales the time by an arbitary factor. This is actually dividing
the times by this value. Alternatively there is the variable self.baseunit. The latter only affects
what is written on the axis, while this value is actually used to scale the times. None (Default)
ignores this
shift_times_by : None, float (optional)
This a value by which the time axis is shifted during import. This is a useful option of e.g.
the recording software does not compensate for t0 and the data is always shifted.
None (Default) ignores this setting
external_time : None or str (optional)
Here a filename extension (string) can be given that contains the time vector.
The file is assumed to be at the same path as the data and to contain a single
type of separated data without header.
If use_same_name = True (default)
It assumes that this is the ending for the file. The filename itself is taken from the filename.
e.g. if samp1.txt is the filename and external_time='.tid' the program searches
samp1.tid for the times. The transpose setting is applied and sets where the times are
to be inserted (row or column indexes)
If use_same_name = False this should be the file containing the vector for the time (in the same format as the main file)
external_wave : None or str (optional)
Here a filename extension (string) can be given that contains the wavelength vector.
If use_same_name = True (default)
The file is assumed to be at the same path as the data and to contain a single type
of separated data without header. This is the ending for the file. The filename itself
is taken from the filename. e.g. if samp1.txt is the filename and external_wave='.wav'
then the program searches samp1.wav for the wavelength. The transpose setting is applied
and sets where the wavelength are to be inserted (columns or row indexes)
If use_same_name = False
this should be a full filename that contains the vector
use_same_name : bool, optional
this switches if the external filename included the loaded filename or is a separate file True(default)
conversion_function: function(optional)
function that receives should have the shape:
return pandas Dataframe with time/frames in rows and wavelength/energy in columns,
The function is tested to accept (in that order) a
my_function(filename, external_time,external_wave),
my_function(filename, external_time),
my_function(filename,external_wave),
my_function(filename) and
return: the dataframe ds with the time_axis as rows and spectral axis as columns
if the ds.index.name ia not empty the "time axis" is in to that name the spectral axis is in ds.columns.name
the return is investigated if it is one, two, or three things.
if two are returned then the second must be the name of what the intensity axis is. This value will then be set to data_type
if three are returned the third is the baseunit (for the time axis) this allows to use the automatic naming in ps or nanosecond
If the values units, data_type or baseunit are (manually) set in the import function the corresponding entries in
datafram will be overwritten
shift_times_by and divide_times_by will be applied if not None (useful to adjust for offset before chirp correction)
return_ds_only: boolean,(optional)
if False (Dafault) returns a TA object, otherwise just a DataFrame
fitcoeff: list, optional
these should be the shirp parameteres that are to be applied to all sub scans in the list.
base_TA_object: TA object, optional
instead of the fit_coefficients a Ta object can be provided that is then used as a template, meaning that the scattercuts and bordercuts will be applied before the filtering.
value_filter : None, float or iterable with two entries, optional
if float, everything above that value or below -abs(value_filter) will be filtered replaced with replace_values
if iterable, then first is lower treshold, second is upper treshold
zscore_filter_level : float, optional
if this value is set then the manual selection will be replaced with an automatic filter, the following options, dump_times = True,
replace_values = None, drop_scans = False decide what is done to the values that are filtered
typical value would be e.g. 3
zscore_in_window : bool,
decides if the filter is applied in the windows or over the whole matrix (using statistics on the values)
dump_times : bool,optional
Standard True means that if the zscore filter filters a file the bad time is droped for the average
replace_values : None, float, optional
if dump times is False the values will be replaced with this value. = None, drop_scans = False
drop_scans : bool,optional
Default: = False. This is the harshest type to filter and means that the whole scan is dropped
Returns
---------
TA object if return_ds_only is False(Default) averaged dataset (ds) of the selected scans or
(if return_removed_list = True) the list of removed scans.
Examples
----------
Use use a range to select the rejected scans, look on the scans by integrating the window 0.5ps to 1ps and 450nm to 470nm
>>> import plot_func as pf #import the module
>>> window1=[0.5,1,450,470] #define the window
>>> #use a 'GUI' to select the files
>>> pf.Summarize_scans(list_of_scans='gui',window1=window1)
>>> #use all scans in the subfolder scans that have the word 'Scan' in them and use the ending 'SIA'
>>> pf.Summarize_scans(path_to_scans = 'Scans', filepart_name = 'Scan', window1=window1)
>>> #This does the same as these are standard
>>> pf.Summarize_scans(window1=window1)
'''
if (base_TA_object is not None) and (conversion_function is None):
if units is None:units=base_TA_object.ds.columns.name
if baseunit is None:baseunit=base_TA_object.ds.index.name
debug = True
if list_of_scans is None:
scan_path=check_folder(path = path_to_scans, current_path = os.getcwd())
if filename_part is not None:#we specified a specific name and want only the files with this name in it
list_of_scans = sorted([os.path.join(scan_path, name) for name in os.listdir(scan_path) if
name.endswith(fileending) and filename_part in name])
else:#we have not specified a specific name and want all files in the folder
list_of_scans = sorted([currentFile for currentFile in scan_path.glob("*.%s"%fileending)])
elif list_of_scans == 'gui':
root_window = tkinter.Tk()
root_window.withdraw()
root_window.attributes('-topmost',True)
root_window.after(1000, lambda: root_window.focus_force())
path_list = filedialog.askopenfilename(initialdir = os.getcwd(),multiple = True,filetypes = [('Raw scan files',"*.%s"%fileending)])
list_of_scans = path_list
elif not hasattr(list_of_scans,'__iter__'):
raise ValueError('We need something to iterate for the list')
if not isinstance(list_of_scans[0],TA):#we do not have opened file but most likely a list of names
try:
list_of_projects = []
for entrance in list_of_scans:
listen = os.path.split(entrance)
path = os.path.normpath(listen[0])
filename = listen[1]
new_ds=TA(filename = filename,path = path, sep = sep, decimal = decimal,
index_is_energy = index_is_energy, transpose = transpose,
sort_indexes = sort_indexes, divide_times_by = divide_times_by,
shift_times_by = shift_times_by, external_time = external_time,
external_wave = external_wave, use_same_name = use_same_name,
data_type = data_type, units = units, baseunit = baseunit,
conversion_function = conversion_function).ds
if base_TA_object is None:
if fitcoeff is not None:
new_ds=Fix_Chirp(ds=new_ds,fitcoeff=fitcoeff)
list_of_projects.append(new_ds.values)
else:
if fitcoeff is not None:
new_ds=Fix_Chirp(ds=new_ds,fitcoeff=fitcoeff)
try:
new_ds=sub_ds(new_ds, ignore_time_region = base_TA_object.ignore_time_region, wave_nm_bin = base_TA_object.wave_nm_bin, baseunit = base_TA_object.baseunit,
scattercut = base_TA_object.scattercut, bordercut = base_TA_object.bordercut, timelimits = base_TA_object.timelimits, time_bin = base_TA_object.time_bin,
equal_energy_bin = base_TA_object.equal_energy_bin)
if (base_TA_object.wave_nm_bin is not None) or (base_TA_object.equal_energy_bin is not None):
print('in the original TA objec the data was rebinned, which is now also done for the single scans. To avoid that use "ta.wave_nm_bin = None" and / or "ta.equal_energy_bin = None" before handing it to base_TA_object')
except:
print('applying the base_TA_object slices failed')
list_of_projects.append(new_ds.values)
if base_TA_object is None:
ds = TA(filename = filename,path = path, sep = sep, decimal = decimal,
index_is_energy = index_is_energy, transpose = transpose, sort_indexes = sort_indexes,
divide_times_by = divide_times_by, shift_times_by = shift_times_by,
external_time = external_time, external_wave = external_wave,
use_same_name = use_same_name, data_type = data_type, units = units,
baseunit = baseunit, conversion_function = conversion_function).ds
else:
ds=base_TA_object.ds
ds = sub_ds(ds, ignore_time_region = base_TA_object.ignore_time_region, wave_nm_bin = base_TA_object.wave_nm_bin, baseunit = base_TA_object.baseunit,
scattercut = base_TA_object.scattercut, bordercut = base_TA_object.bordercut, timelimits = base_TA_object.timelimits, time_bin = base_TA_object.time_bin,
equal_energy_bin = base_TA_object.equal_energy_bin)
######################
try:
list_of_projects = np.transpose(np.array(list_of_projects),(1, 2, 0))
except:
print('the stacking of the scans failed, are you sure that all are have the same shape')
#######################
except:
raise ValueError('Sorry did not understand the project_list entry, use GUI_open to create one')
else:
try:
list_of_projects = []
list_of_scans_names = []
for entrance in list_of_scans:
list_of_projects.append(entrance.ds.values)
list_of_scans_names.append(entrance.filename)
if base_TA_object is None:
ds = list_of_scans[0]
else:
ds=base_TA_object.ds
list_of_scans = list_of_scans_names
##########################
try:
list_of_projects = np.transpose(np.array(list_of_projects),(1, 2, 0))
except:
print('the stacking of the scans failed, are you sure that all are have the same shape')
#########################
except:
raise ValueError('Sorry did not understand the project_list entry, use GUI_open to create one')
if window1 is None:
window1 = [ds.index.values.min(),ds.index.values.max(),ds.columns.values.min(),ds.columns.values.max()]
#### automatic filtering#####
if (zscore_filter_level is not None) or (value_filter is not None):
if replace_values is not None:
cut_bad_times=False
if replace_values is None:
replace_values = np.nan
dataset=list_of_projects
if value_filter is not None:
if hasattr(value_filter,'__iter__'):
lowervalue=value_filter[0]
uppervalue=value_filter[1]
else:
uppervalue = np.abs(value_filter)
lowervalue = -np.abs(value_filter)
outside_range=np.invert(log_and(dataset>lowervalue,dataset<uppervalue))
if dump_times:#this is default
outside_range=np.tile(outside_range.all(axis=1,keepdims=True),(1,dataset.shape[1],1))
elif drop_scans:
outside_range=np.tile(outside_range.any(axis=1,keepdims=True),(1,dataset.shape[1],1))
outside_range=np.tile(outside_range.any(axis=0,keepdims=True),(dataset.shape[0],1,1))
dataset[outside_range]=replace_values
if zscore_filter_level is not None:
if zscore_in_window:
window1_index = [find_nearest_index(ds.index.values,window1[0]),find_nearest_index(ds.index.values,window1[1]),find_nearest_index(ds.columns.values,window1[2]),find_nearest_index(ds.columns.values,window1[3])]
vector=np.nanmean(np.nanmean(dataset[window1_index[0]:window1_index[1],window1_index[2]:window1_index[3],:],axis=0),axis=1)
good=log_and(vector>(np.nanmean(vector) - zscore_filter_level*np.nanstd(vector)),vector<(np.nanmean(vector) + zscore_filter_level*np.nanstd(vector)))
if not window2 is None:
window2_index = [find_nearest_index(ds.index.values,window2[0]),find_nearest_index(ds.index.values,window2[1]),find_nearest_index(ds.columns.values,window2[2]),find_nearest_index(ds.columns.values,window2[3])]
vector=np.nanmean(np.nanmean(dataset[window2_index[0]:window2_index[1],window2_index[2]:window2_index[3],:],axis=0),axis=1)
good2=log_and(vector>(np.nanmean(vector) - zscore_filter_level*np.nanstd(vector)),vector<(np.nanmean(vector) + zscore_filter_level*np.nanstd(vector)))
good=log_and(good,good2)
dataset[:,:,np.invert(good)]=replace_values
else:
mean=np.nanmean(dataset,axis=2)
var=np.nanstd(dataset,axis=2)
lower=(mean - zscore_filter_level*var).T
upper=(mean + zscore_filter_level*var).T
lower=np.array([lower for i in range(dataset.shape[2])]).T
upper=np.array([upper for i in range(dataset.shape[2])]).T
outside_range=np.invert(log_and(dataset>lower,dataset<upper))
if drop_scans:
outside_range=np.tile(outside_range.any(axis=1,keepdims=True),(1,dataset.shape[1],1))
outside_range=np.tile(outside_range.any(axis=0,keepdims=True),(dataset.shape[0],1,1))
elif dump_times:
outside_range=np.tile(outside_range.any(axis=1,keepdims=True),(1,dataset.shape[1],1))
dataset[outside_range]=replace_values
list_of_projects=dataset
#############manual filtering################
else:
if baseunit is None:baseunit=ds.index.name
if units is None:units=ds.columns.name
if list_to_dump is not None:
if list_to_dump == 'single':
print('we will use a gui to select single scans to extract')
elif list_to_dump == 'range':
print('we will use a gui to select the first and last scan to remove')
else:
if not hasattr(list_to_dump,'__iter__'):#we have only a single number/name in there
list_to_dump = [list_to_dump]
filenames_to_dump = []
for entry in list_to_dump:
try:
filenames_to_dump.append(list_of_scans[entry].filename) #list_of_scans is a list of TA objects that have filename and if entry is an index of this list this goes well
except:
filenames_to_dump.append(entry)# we assume it is already a filename
list_to_dump = []
for filename in filenames_to_dump:
list_to_dump.append(list_of_scans.index(filename))
for i in range(30):#we make a maximum of 30 rounds
window1_index = [find_nearest_index(ds.index.values,window1[0]),find_nearest_index(ds.index.values,window1[1]),find_nearest_index(ds.columns.values,window1[2]),find_nearest_index(ds.columns.values,window1[3])]
series1 = pandas.Series(list_of_projects[window1_index[0]:window1_index[1],window1_index[2]:window1_index[3],:].mean(axis = (0,1)))
series1.name = '%.3g:%.3g %s at %.1f:%.1f %s'%(window1[0],window1[1],baseunit,window1[2],window1[3],units)
if not window2 is None:
window2_index = [find_nearest_index(ds.index.values,window2[0]),find_nearest_index(ds.index.values,window2[1]),find_nearest_index(ds.columns.values,window2[2]),find_nearest_index(ds.columns.values,window2[3])]
series2 = pandas.Series(list_of_projects[window2_index[0]:window2_index[1],window2_index[2]:window2_index[3],:].mean(axis = (0,1)))
series2.name = '%.3g:%.3g %s at %.1f:%.1f %s'%(window2[0],window2[1],baseunit,window2[2],window2[3],units)
fig,(ax,ax2) = plt.subplots(2,1,sharex = True,figsize = (16,12))
series1.plot(ax = ax,color = colm(1),use_index = False)
series2.plot(ax = ax2,color = colm(3),use_index = False)
if len(series1) >15:
gol=Frame_golay(series1,window=11,order=1)
gol.plot(ax=ax,use_index=False,color=colm(2))
ax.fill_between(x=range(len(series1)), y1=gol-series1.var(), y2=gol+series1.var(),color=colm(2),alpha=0.3)
gol=Frame_golay(series2,window=11,order=1)
gol.plot(ax=ax2,use_index=False,color=colm(4))
ax2.fill_between(x=range(len(series1)), y1=gol-2*series1.var(), y2=gol+2*series1.var(),color=colm(4),alpha=0.3)
else:
fig,ax=plt.subplots(1,1,sharex=True,figsize=(16,12))
series1.plot(ax=ax,color=colm(1),use_index=False)
if len(series1) >15:
gol=Frame_golay(series1,window=11,order=1)
gol.plot(ax=ax,use_index=False,color=colm(2))
#try:
ax.fill_between(x=range(len(series1)), y1=gol-2*np.nanvar(series1.values), y2=gol+2*np.nanvar(series1.values),color=colm(2),alpha=0.3)
#except:
# pass
if list_to_dump == 'single':
ax.set_title('click on the scans that should be dropped\n left click to chose, right click to delete last point, middle click finishes selection\n an empty middle click ends the process')
polypts=np.asarray(plt.ginput(n=int(len(series1)/2),timeout=300, show_clicks=True,mouse_add=1, mouse_pop=3, mouse_stop=2))
if len(polypts)<1:break
to_remove=[int(a) for a in np.array(polypts)[:,0]]
remove=pandas.Series(np.arange(len(series1)))+1
remove[to_remove]=0
to_remove=list(remove[remove<1].index.values)
to_keep=list(remove[remove>1].index.values)
elif (list_to_dump == 'range') or (i>0):
ax.set_title('click on the first and last scan to be removed, repeat as long as necessary\n an empty middle click ends the process')
polypts=np.asarray(plt.ginput(n=int(len(series1)/2),timeout=300, show_clicks=True,mouse_add=1, mouse_pop=3, mouse_stop=2))
if len(polypts)<1:break
polypts=np.array(polypts)[:,0]
remove=pandas.Series(np.arange(len(series1)))+1
for i in range(int(len(polypts)/2)):
remove.loc[polypts[2*i]:polypts[2*i+1]]=0
to_remove=list(remove[remove<1].index.values)
to_keep=list(remove[remove>1].index.values)
elif i == 0:
to_keep=list(range(len(series1)))
to_keep.remove(list_to_dump)
else:
raise ValueError('Something is weired')
list_of_projects=list_of_projects[:,:,to_keep]
plt.close('all')
plt.close('all')
if 0:
try:
df=pandas.DataFrame(np.any(np.isnan(dataset),axis=1),index=ds.index)
plot2d(df,levels = 2,use_colorbar = False,intensity_range=[0,1],title='rejected are red')
except Exception as e:
print('plotting of filtered went wrong\n Error message was: \n')
print(e)
try:
ds=pandas.DataFrame(np.nanmean(list_of_projects,axis=2),index=ds.index,columns=ds.columns)
except:
print('nanmean failed, assume single wavelength')
ds=pandas.DataFrame(np.mean(list_of_projects),index=ds.index,columns=ds.columns)
if not save_name is None:
path = str(check_folder(path=path,filename=save_name))
ds.to_csv(path,sep='\t')
ta=TA(path)
else:
path = str(check_folder(path=path,filename='temp_combined.SIA'))
ds.to_csv(path,sep='\t')
ta=TA(path)
try:
os.remove(path)
except:
print('could not remove temp_combined.SIA')
if return_ds_only:
return ds
elif return_removed_list:
return filenames_to_dump
else:
if base_TA_object is not None:
ta=base_TA_object.Copy()
ta.ds_ori=ds
ta.ds=ds
return ta
def sub_ds(ds, times = None, time_width_percent = 0, ignore_time_region = None, drop_ignore=False, wave_nm_bin = None,
baseunit = None, scattercut = None, drop_scatter=False, bordercut = None, timelimits = None, wavelength_bin = None,
wavelength = None, time_bin = None, equal_energy_bin = None, from_fit = False):
'''This is the main function that creates all the slices of the data matrix
Parameters
---------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
times : float or list/vector (of floats), optional
For each entry in rel_time a spectrum is plotted. If time_width_percent=0 (Default) the
nearest measured timepoint is chosen. For other values see 'time_width_percent'
time_width_percent : float
"rel_time" and "time_width_percent" work together for creating spectral plots at
specific timepoints. For each entry in rel_time a spectrum is plotted.
If however e.g. time_width_percent=10 the region between the timepoint closest
to the 1.1 x timepoint and 0.9 x timepoint is averaged and shown
(and the legend adjusted accordingly). This is particularly useful for the densly
sampled region close to t=0. Typically for a logarithmic recorded kinetics, the
timepoints at later times will be further appart than 10 percent of the value,
but this allows to elegantly combine values around time=0 for better statistics.
This averaging is only applied for the plotting function and not for the fits.
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
drop_ignore : Bool, True or False, optional
If set to True the values in ignore_time_region are removed from the dataset instead of set to zero
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
equal_energy_bin : None or float(optional)
if this is set the wave_nm_bin is ignored and the data is rebinned into equal energy bins.
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
drop_scatter : Bool, True or False, optional
If set to True the values in scattercut are removed from the dataset instead of set to zero
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
wavelength : float or list (of floats), optional
'wavelength' and 'wavelength_bin' work together for the creation
of kinetic plots. When plotting kinetic spectra one line will be plotted for each entrance
in the list/vector rel_wave. During object generation the vector np.arange(300,1000,100)
is set as standard. Another typical using style would be to define a list of interesting
wavelength at which a kinetic development is to be plotted. At each selected wavelength
the data between wavelength+ta.wavelength_bin and wavelength-ta.wavelength_bin is averaged
for each timepoint returned
wavelength_bin : float, optional
the width used in kinetics, see below (Default) None
time_bin : None or int, optional
is dividing the points on the time-axis in even bins and averages the found values in between.
This is a hard approach that also affects the fits. I do recommend to use this carefully,
it is most useful for modulated data. A better choice for transient absorption that only
affects the kinetics is 'time_width_percent'
'''
time_label=ds.index.name
energy_label=ds.columns.name
if (wavelength is not None) and (times is not None):raise ValueError('can not get wavelength and times back')
if (bordercut is not None) and not from_fit:
ds.columns=ds.columns.astype('float')
ds=ds.loc[:,bordercut[0]:bordercut[1]]
if (equal_energy_bin is not None) and (wavelength is None):# we work with optical data but want to bin in equal energy
x=ds.columns.values.astype('float')
y=ds.index.values.astype('float')
energy_label='Energy in eV'
x=scipy.constants.h*scipy.constants.c/(x*1e-9*scipy.constants.electron_volt)
if from_fit:#they are already binned
ds.columns=x
ds.sort_index(axis=1,ascending=False)
elif (x[1:]-x[:-1]>equal_energy_bin).all():
raise ValueError("equal_energy_bin bins are to small for the data")
else:
rebin_max=np.argmin((x[1:]-x[:-1])<equal_energy_bin)#find the position where the difference is larger than the wave_nm_bin
if rebin_max==0:rebin_max=len(x)# we get 0 when all teh values are ok
if rebin_max<len(x):
if (x[1:]-x[:-1]>equal_energy_bin).all():raise ValueError("equal_energy_bin bins are to small for the data")
bins=np.arange(x.min(),x[rebin_max],equal_energy_bin)
bin_means,bin_edges = binned_statistic(x[:rebin_max], ds.values[:,:rebin_max], statistic='mean',bins=bins)[:2]
bins=(bin_edges[1:]+bin_edges[:-1])/2.
ds=pandas.concat((pandas.DataFrame(bin_means,index=y,columns=bins),ds.iloc[:,rebin_max:]), axis=1, join='outer')
else:
bins=np.arange(x.min(),x.max()+equal_energy_bin,equal_energy_bin)
bin_means,bins = binned_statistic(x, ds.values, statistic='mean',bins=bins)[:2]
bins=(bins[1:]+bins[:-1])/2.
ds=pandas.DataFrame(bin_means,index=y,columns=bins)
elif (wave_nm_bin is not None) and (wavelength is None):# bin in wavelength
x=ds.columns.values.astype('float')
y=ds.index.values.astype('float')
if (x[1:]-x[:-1]>wave_nm_bin).all():raise ValueError("wavelength_nm_bins bins are to small for the data")
rebin_max=np.argmin((x[1:]-x[:-1])<wave_nm_bin)#find the position where the difference is larger than the wave_nm_bin
if rebin_max==0:rebin_max=len(x)# we get 0 when all teh values are ok
if rebin_max<len(x):
if (x[1:]-x[:-1]>wave_nm_bin).all():raise ValueError("wavelength_nm_bins bins are to small for the data")
bins=np.arange(x.min(),x[rebin_max],wave_nm_bin)
bin_means,bin_edges = binned_statistic(x[:rebin_max], ds.values[:,:rebin_max], statistic='mean',bins=bins)[:2]
bins=(bin_edges[1:]+bin_edges[:-1])/2.
ds=pandas.concat((pandas.DataFrame(bin_means,index=y,columns=bins),ds.iloc[:,rebin_max:]), axis=1, join='outer')
else:
bins=np.arange(x.min(),x.max()+wave_nm_bin,wave_nm_bin)
bin_means,bins = binned_statistic(x, ds.values, statistic='mean',bins=bins)[:2]
bins=(bins[1:]+bins[:-1])/2.
ds=pandas.DataFrame(bin_means,index=y,columns=bins)
if time_bin is not None:
time=ds.index.values.astype('float')
y=ds.columns.values.astype('float')
time_bin=int(time_bin)
time_bins=time[::time_bin]
bin_means,bins = binned_statistic(time, ds.values.T, statistic='mean',bins=time_bins)[:2]
bins=(bins[1:]+bins[:-1])/2.
ds=pandas.DataFrame(bin_means,index=y,columns=bins)
ds=ds.T
if timelimits is not None:
ds.index=ds.index.astype('float')
ds=ds.loc[timelimits[0]:timelimits[1],:]
if ignore_time_region is not None:
ds=ds.fillna(value=0)
ds.index=ds.index.astype('float')
if isinstance(ignore_time_region[0], numbers.Number):
if drop_ignore:
ds.loc[ignore_time_region[0]:ignore_time_region[1],:]=np.nan
else:
ds.loc[ignore_time_region[0]:ignore_time_region[1],:]=0
else:
try:
for entries in ignore_time_region:
if drop_ignore:
ds.loc[entries[0]:entries[1],:]=np.nan
else:
ds.loc[entries[0]:entries[1],:]=0
except:
pass
ds=ds.dropna(axis=0)
if scattercut is not None:
ds=ds.fillna(value=0)
x=ds.columns.values.astype('float')
if isinstance(scattercut[0], numbers.Number):
if (equal_energy_bin is not None):
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
scattercut=scattercut[::-1]
lower=find_nearest_index(x,scattercut[0])
upper=find_nearest_index(x,scattercut[1])
if drop_scatter:
ds.iloc[:,lower:upper]=np.nan
else:
ds.iloc[:,lower:upper]=0
else:
try:
for entries in scattercut:
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
scattercut=scattercut[::-1]
lower=find_nearest_index(x,entries[0])
upper=find_nearest_index(x,entries[1])
if drop_scatter:
ds.iloc[:,lower:upper]=np.nan
else:
ds.iloc[:,lower:upper]=0
except:
pass
ds=ds.dropna(axis=1)
#until here we always have the same matrix
ds.index.name=time_label
ds.columns.name=energy_label
if wavelength is not None:#ok we want to have singular wavelength
if not hasattr(wavelength,'__iter__'):wavelength=np.array([wavelength])
if len(wavelength)>1:wavelength.sort()
for i,wave in enumerate(wavelength):
upper=wave+wavelength_bin/2
lower=wave-wavelength_bin/2
if equal_energy_bin is not None and from_fit:
upper=scipy.constants.h*scipy.constants.c/(lower*1e-9*scipy.constants.electron_volt)
lower=scipy.constants.h*scipy.constants.c/(upper*1e-9*scipy.constants.electron_volt)
wave=scipy.constants.h*scipy.constants.c/(wave*1e-9*scipy.constants.electron_volt)
if i == 0:
out=ds.loc[:,lower:upper].mean(axis='columns').to_frame()
out.columns = [wave]
else:
if wave in out.columns:continue
out[wave] = ds.loc[:,lower:upper].mean(axis='columns')
out.columns=out.columns.astype('float')
out.columns.name=energy_label
out.index.name=time_label
ds=out
if times is not None: #ok we want to have single times
if not hasattr(times, '__iter__'):times=np.array([times])
if baseunit is None:baseunit = 'ps'
time_scale=ds.index.values
if time_width_percent>0:
for i,time in enumerate(times):
if time<0:
limits = [find_nearest_index(time_scale,time+time*time_width_percent/100.),
find_nearest_index(time_scale,time-time*time_width_percent/100.)]
else:
limits = [find_nearest_index(time_scale,time-time*time_width_percent/100.),
find_nearest_index(time_scale,time+time*time_width_percent/100.)]
time_lower = time_scale[limits[0]]
time_upper = time_scale[limits[1]]
time_mean = (time_lower+time_upper)/2
if i == 0:
out=ds.iloc[limits[0]:limits[1],:].mean(axis='rows').to_frame()
out.columns = ['%.3g %s (%.3g - %.3g %s)'%(time_mean,baseunit,time_lower,time_upper,baseunit)]
else:
out['%.3g %s (%3g - %.3g %s)'%(time_mean,baseunit,time_lower,time_upper,baseunit)]=ds.iloc[limits[0]:limits[1],:].mean(axis='rows').to_frame()
else:
for i,time in enumerate(times):
index=find_nearest_index(time_scale,time)
if i == 0:
out=ds.iloc[index,:].to_frame()
out.columns=['%.3g %s'%(time_scale[index],baseunit)]
else:
out['%.3g %s'%(time_scale[index],baseunit)]=ds.iloc[index,:]
out.columns.name=time_label
out.index.name=energy_label
ds=out
#ds.index.name='Wavelength in nm'
ds.fillna(value=0,inplace=True)#lets fill nan values with zero to catch problems
if equal_energy_bin is not None:
ds.sort_index(axis=1,inplace=True,ascending=False)
return ds
[docs]def plot2d(ds, ax = None, title = None, intensity_range = None, baseunit = 'ps', timelimits = None,
scattercut = None, bordercut = None, wave_nm_bin = None, ignore_time_region = None,
time_bin = None, log_scale = False, plot_type = 'symlog', lintresh = 1,
wavelength_bin = None, levels = 256, use_colorbar = True, cmap = None,
data_type = 'differential Absorption in $\mathregular{\Delta OD}$', equal_energy_bin = None, from_fit = False):
'''function for plotting matrix of TA data.
Parameters
---------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
ax : None, matplotlib axis object optional
If None (Default) a new plot is is created and a new axis, otherwise ax needs to be Matplotlib Axis
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
title : None or str
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will
be used in all plots. To remove the title all together set an empty string with this command title=""
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
equal_energy_bin : None or float(optional)
if this is set the wave_nm_bin is ignored and the data is rebinned into equal energy bins (based upon that the data is in nm.
If dual axis is on then the lower axis is energy and the upper is wavelength
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
time_bin : None or int, optional
is dividing the points on the time-axis in even bins and averages the found values in between.
This is a hard approach that also affects the fits. I do recommend to use this carefully,
it is most useful for modulated data. A better choice for transient absorption that only
affects the kinetics is 'time_width_percent'
log_scale : bool, optional
If True (Default), The 2D plots (Matrix) is plotted with a pseudo logarithmic intensity scale.
This usually does not give good results unless the intensity scale is symmetric
plot_type : None or str
is a general setting that can influences what time axis will be used for the plots.
"symlog" (linear around zero and logarithmic otherwise) "lin" and "log" are valid options.
lintresh : float
The pseudo logratihmic range "symlog" is used for most time axis. Symlog plots a range around
time zero linear and beyond this linear treshold 'lintresh' on a logarithmic scale. (Default) 0.3
wavelength_bin : float, optional
the width used in kinetics, see below (Default) 10nm
levels : int, optional
how many different colours to use in the description. less makes for more contrast but less
intensity details (Default) 256
use_colorbar : bool, optional
if True (Default) a colour bar is added to the 2d plot for intensity explanation, switch
mostely used for creating multiple plots
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
from_fit : bool optional
it needed this swtich to avoid re-slicing of data in spectal axis for equal energy bins
'''
if cmap is None:
cmap=standard_map
elif not np.array([isinstance(cmap,type(cm.viridis)),isinstance(cmap,type(cm.jet)),isinstance(cmap,type(cm.Blues)),isinstance(cmap,type(cm.coolwarm)),isinstance(cmap,type(cm.terrain))]).any():#we must have a
cmap=standard_map
if ax is None:
ax_ori=False
fig,ax=plt.subplots(figsize=(10,6),dpi=100)
else:
ax_ori=True
fig=ax.get_images()
if timelimits is None:
timelimits=(ds.index.min(),ds.index.max())
ds = sub_ds(ds, scattercut = scattercut, bordercut = bordercut, timelimits = timelimits, wave_nm_bin = wave_nm_bin,
wavelength_bin = wavelength_bin, time_bin = time_bin, ignore_time_region = ignore_time_region,
drop_scatter = False, drop_ignore = False, equal_energy_bin = equal_energy_bin, from_fit = from_fit)
if intensity_range is None:
try:
maxim=max([abs(ds.values.min()),abs(ds.values.max())])
intensity_range=[-maxim,maxim]
except:
intensity_range=[-1e-2,1e-2]
else:
if not hasattr(intensity_range,'__iter__'):#lets have an lazy option
intensity_range=[-intensity_range,intensity_range]
else:
if log_scale:print('I highly recommend to make a symmetric intensity distribution for logarithmic scale, the colorbar might look strange otherwise')
if log_scale:
if 0:# old manual symlog
bounds0 = list(-1*np.logspace(np.log10(-intensity_range[0]), np.log10(-intensity_range[0]/(levels/2)), levels))
bounds1 = np.logspace(np.log10(intensity_range[1]/(levels/2)),np.log10(intensity_range[1]), levels)
bounds0.append(0)
for a in bounds1:
bounds0.append(a)
norm = BoundaryNorm(boundaries=bounds0, ncolors=len(bounds0))
mid_color=colm(k=range(levels),cmap=cmap)
else:
norm=SymLogNorm(abs(max(intensity_range)-min(intensity_range))/100, linscale=1.0, vmin=min(intensity_range), vmax=max(intensity_range), clip=True, base=10)
mid_color=cmap(0.5)
else:
if 0:
nbins=levels
levels = LinearLocator(numticks=levels).tick_values(vmin=min(intensity_range), vmax=max(intensity_range))
norm = BoundaryNorm(levels,clip=True,ncolors=cmap.N)
mid_color_index=find_nearest_index(0,levels)
mid_color=colm(k=range(nbins),cmap=cmap)
mid_color=mid_color[mid_color_index]
else:
norm = Normalize(vmin=min(intensity_range),vmax=max(intensity_range))
mid_color=cmap(0.5)
#print(ds.head())
x = ds.columns.values.astype('float')
y = ds.index.values.astype('float')
X, Y = np.meshgrid(x, y)
img=ax.pcolormesh(X,Y,ds.values,norm=norm,cmap=cmap,shading=shading)
#img=ax.pcolormesh(X,Y,ds.values,cmap=cmap,shading=shading)
if ignore_time_region is None:
pass
elif isinstance(ignore_time_region[0], numbers.Number):
ds.index=ds.index.astype(float)
try:
upper=ds.loc[ignore_time_region[1]:,:].index.values.min()
lower=ds.loc[:ignore_time_region[0],:].index.values.max()
if equal_energy_bin is not None:
rect = plt.Rectangle((x.max(),lower), width=abs(ax.get_xlim()[0]-ax.get_xlim()[1]), height=abs(upper-lower),facecolor=mid_color,alpha=1)#mid_color)
else:
rect = plt.Rectangle((x.min(),lower), width=abs(ax.get_xlim()[1]-ax.get_xlim()[0]), height=abs(upper-lower),facecolor=mid_color,alpha=1)#mid_color)
ax.add_patch(rect)
except:
pass
else:
ignore_time_region_loc=flatten(ignore_time_region)
for k in range(int(len(ignore_time_region_loc)/2+1)):
try:
upper=ds.loc[ignore_time_region[k+1]:,:].index.values.min()
lower=ds.loc[:ignore_time_region[k],:].index.values.max()
if equal_energy_bin is not None:
rect = plt.Rectangle((x.max(),lower), width=abs(ax.get_xlim()[0]-ax.get_xlim()[1]), height=abs(upper-lower),facecolor=mid_color,alpha=1)
else:
rect = plt.Rectangle((x.min(),lower), width=abs(ax.get_xlim()[1]-ax.get_xlim()[0]), height=abs(upper-lower),facecolor=mid_color,alpha=1)
ax.add_patch(rect)
except:
pass
if scattercut is None:
pass
elif isinstance(scattercut[0], numbers.Number):
try:
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
scattercut=scattercut[::-1]
upper=ds.loc[:,scattercut[1]:].columns.values.min()
lower=ds.loc[:,:scattercut[0]].columns.values.max()
width=abs(upper-lower)
rect = plt.Rectangle((lower,y.min()), height=abs(ax.get_ylim()[1]-ax.get_ylim()[0]), width=width, facecolor=mid_color,alpha=1)#mid_color)
ax.add_patch(rect)
except:
pass
else:
scattercut=flatten(scattercut)
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
scattercut=scattercut[::-1]
for k in range(int(len(scattercut)/2+1)):
try:
upper=ds.loc[:,scattercut[k][1]:].columns.values.min()
if upper==0:raise
lower=ds.loc[:,:scattercut[k][0]].columns.values.max()
rect = plt.Rectangle((lower.min()), height=abs(ax.get_ylim()[1]-ax.get_ylim()[0]), width=abs(upper-lower),facecolor=mid_color,alpha=1)#mid_color)
ax.add_patch(rect)
except:
pass
if use_colorbar:
mid=(intensity_range[1]+intensity_range[0])/2
if log_scale:
values=[intensity_range[0],mid-abs(intensity_range[0]-mid)/10,mid-abs(intensity_range[0]-mid)/100,mid,mid+abs(intensity_range[1]-mid)/100,mid+abs(intensity_range[1]-mid)/10,intensity_range[1]]
else:
values=[intensity_range[0],intensity_range[0]+abs(intensity_range[0]-mid)/2,mid,intensity_range[1]-abs(intensity_range[1]-mid)/2,intensity_range[1]]
labels=['%.2g'%(a) for a in values]
labels[0]='<' + labels[0]
labels[-1]='>'+labels[-1]
cbar=plt.colorbar(img, ax=ax,ticks=values,pad=0.01)
cbar.ax.set_yticklabels(labels)
a=ax.yaxis.label
fontsize=a.get_fontsize()
fontsize-=4
if not data_type is None:#we use this as a switch to enable a flexible avoidance of the label setting.
if log_scale:
if ax_ori:cbar.set_label(data_type + '\nLog-scale', rotation=270,labelpad=20,fontsize=fontsize)
else:cbar.set_label(data_type + '\nLog-scale', rotation=270,labelpad=20,fontsize=fontsize)
else:
if ax_ori:cbar.set_label(data_type, rotation=270,labelpad=20,fontsize=fontsize)
else:cbar.set_label(data_type, rotation=270,labelpad=20,fontsize=fontsize)
if "symlog" in plot_type:
ax.plot(ax.get_xlim(),[lintresh,lintresh],'black',lw=0.5,alpha=0.3)
ax.plot(ax.get_xlim(),[-1.0*lintresh,-1.0*lintresh],'black',lw=0.5,alpha=0.3)
ax.plot(ax.get_xlim(),[0,0],'black',lw=0.5,alpha=0.6)
if 1:
ax.set_yscale('symlog', linthresh=lintresh)
locmaj = matplotlib.ticker.LogLocator(base=10.0, subs=(0.1,1.0,10.,1e2,1e3,1e4))
ax.yaxis.set_major_locator(locmaj)
locmin = matplotlib.ticker.LogLocator(base=10.0, subs=np.arange(0.1,1,0.1))
ax.yaxis.set_minor_locator(locmin)
ticks=list(ax.get_yticks())
ticks.append(lintresh)
[ticks.append(a) for a in [-0.3,-1,-2,-5,-10]]
ticks.sort()
if timelimits[1]>100:
ticks=np.array(ticks)
ticks=np.concatenate((ticks.clip(min=0.1),np.zeros(1),ticks.clip(max=-0.1,min=timelimits[0])),axis=0)
ax.set_yticks(ticks)
else:
print('here2')
ax.set_yscale('symlog', linthresh=lintresh,subsy=range(2,9),linscaley=lintresh)
ax.set_ylim(y.min(),y.max())
elif "log" in plot_type:
lower_time=max(1e-6,timelimits[0])
ax.set_ylim(lower_time,y.max())
ax.set_yscale('log')
else:
ax.set_yscale('linear')
ax.set_ylim(timelimits)
if bordercut is not None:
try:
if equal_energy_bin is not None:
bordercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in bordercut]
ax.set_xlim(bordercut[0],bordercut[1])
except:
print('bordercut failed')
pass
if equal_energy_bin is not None and False:
temp=np.array(ax.get_xlim())
ax.set_xlim(temp.max(),temp.min())
ax.set_xlabel(ds.columns.name)
ax.set_ylabel(ds.index.name)
if title:
ax.set_title(title)
if ax_ori:return ax
return fig
[docs]def plot2d_fit(re, error_matrix_amplification=5, use_images=True, patches=False, title = None,
intensity_range = None, baseunit = 'ps', timelimits = None,
scattercut = None, bordercut = None, wave_nm_bin = None, ignore_time_region = None,
time_bin = None, log_scale = False, scale_type = 'symlog', lintresh = 1,
wavelength_bin = None, levels = 256, plot_with_colorbar = True, cmap = None,
data_type = 'differential Absorption in $\mathregular{\Delta OD}$', equal_energy_bin = None):
'''Plots the fit output as a single plot with meas,fitted and difference.
The differnece used err_matrix_amplification as a factor. patches moves the labels from the
title into white patches in the top of the figure
Parameters
---------------
re : dict
Dictionary that contains the fit results and specific the dataframes A, AC and AE
error_matrix_amplification : int, optional
the error matrix AE is multiplied by this factor for the plot.
use_images : bool:
(Default)True converts the matrix into images, to reduce the filesize.
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
patches : bool, optional
If False (Default) the names "measured" "fitted" "difference" will be placed above the images.
If True, then they will be included into the image (denser)
title : None or str
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will
be used in all plots. To remove the title all together set an empty string with this command title=""
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
equal_energy_bin : None or float(optional)
if this is set the wave_nm_bin is ignored and the data is rebinned into equal energy bins (based upon that the data is in nm.
If dual axis is on then the lower axis is energy and the upper is wavelength
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
time_bin : None or int, optional
is dividing the points on the time-axis in even bins and averages the found values in between.
This is a hard approach that also affects the fits. I do recommend to use this carefully,
it is most useful for modulated data. A better choice for transient absorption that only
affects the kinetics is 'time_width_percent'
log_scale : bool, optional
If True (Default), The 2D plots (Matrix) is plotted with a pseudo logarithmic intensity scale.
This usually does not give good results unless the intensity scale is symmetric
Scale_type : None or str
is a general setting that can influences what time axis will be used for the plots.
"symlog" (linear around zero and logarithmic otherwise) "lin" and "log" are valid options.
lintresh : float
The pseudo logratihmic range "symlog" is used for most time axis. Symlog plots a range around
time zero linear and beyond this linear treshold 'lintresh' on a logarithmic scale. (Default) 0.3
wavelength_bin : float, optional
the width used in kinetics, see below (Default) 10nm
levels : int, optional
how many different colours to use in the description. less makes for more contrast but less
intensity details (Default) 256
plot_with_colorbar : bool, optional
if True (Default) a colour bar is added to the 2d plot for intensity explanation, switch
mostely used for creating multiple plots
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
'''
if intensity_range is None:intensity_range=5e-3
fig,ax=plt.subplots(3,figsize=(9,11))
if patches:
plot2d(re['A'], cmap = cmap, log_scale = log_scale, intensity_range = intensity_range, ax = ax[0],
baseunit = baseunit, use_colorbar = plot_with_colorbar, levels = levels, plot_type = scale_type,
ignore_time_region = ignore_time_region, lintresh = lintresh, bordercut = bordercut,
scattercut = scattercut, timelimits = timelimits, data_type = data_type, equal_energy_bin = equal_energy_bin, from_fit = True)
plot2d(re['AC'], cmap = cmap, log_scale = log_scale, intensity_range = intensity_range, ax = ax[1],
baseunit = baseunit, use_colorbar = plot_with_colorbar, levels = levels, plot_type = scale_type,
ignore_time_region = ignore_time_region, lintresh = lintresh, bordercut = bordercut,
scattercut = scattercut, timelimits = timelimits, data_type = data_type, equal_energy_bin = equal_energy_bin, from_fit = True)
plot2d(re['AE'], cmap = cmap, log_scale = log_scale, intensity_range = np.array(intensity_range)/error_matrix_amplification, ax = ax[2],
baseunit = baseunit, use_colorbar = plot_with_colorbar, levels = levels, plot_type = scale_type,
ignore_time_region = ignore_time_region, lintresh = lintresh, bordercut = bordercut, scattercut = scattercut,
timelimits = timelimits, data_type = data_type, equal_energy_bin = equal_energy_bin, from_fit = True)
for i in range(3):
ax[i].set_title(label='')
stringen=['measured','calculated','difference']
x_width=(ax[i].get_xlim()[1]-ax[i].get_xlim()[0])/4
if 'lin' in scale_type:
y_width=(ax[i].get_ylim()[1])/8
else:
y_width=(ax[i].get_ylim()[1])/1.5
rect = plt.Rectangle((ax[i].get_xlim()[1]-x_width, ax[i].get_ylim()[1]-y_width), x_width, y_width,facecolor="white", alpha=0.5)
ax[i].add_patch(rect)
ax[i].text(ax[i].get_xlim()[1]-x_width+x_width*0.1,ax[i].get_ylim()[1]-y_width+y_width*0.1,stringen[i],fontsize=16)
fig.subplots_adjust(left=0.15, bottom=0.067, right=0.97, top=0.985, wspace=0.0, hspace=0.258)
else:
plot2d(re['A'], cmap = cmap, title = 'Measured', log_scale = log_scale, intensity_range = intensity_range,
ax = ax[0], baseunit = baseunit, use_colorbar = plot_with_colorbar, levels = levels, plot_type = scale_type,
ignore_time_region = ignore_time_region, lintresh = lintresh, bordercut = bordercut, scattercut = scattercut,
timelimits = timelimits, data_type = data_type, equal_energy_bin = equal_energy_bin, from_fit = True)
plot2d(re['AC'], cmap = cmap, title = 'Calculated', log_scale = log_scale, intensity_range = intensity_range,
ax = ax[1], baseunit = baseunit, use_colorbar = plot_with_colorbar, levels = levels, plot_type = scale_type,
ignore_time_region = ignore_time_region, lintresh = lintresh, bordercut = bordercut, scattercut = scattercut,
timelimits = timelimits , data_type = data_type, equal_energy_bin = equal_energy_bin, from_fit = True)
plot2d(re['AE'], cmap = cmap, title = 'Difference', log_scale = log_scale, intensity_range = np.array(intensity_range)/error_matrix_amplification,
ax = ax[2], baseunit = baseunit, use_colorbar = plot_with_colorbar, levels = levels, plot_type = scale_type,
ignore_time_region = ignore_time_region, lintresh = lintresh, bordercut = bordercut, scattercut = scattercut,
timelimits = timelimits, data_type = data_type, equal_energy_bin = equal_energy_bin, from_fit = True)
#fig.subplots_adjust(left=0.15, bottom=0.067, right=0.97, top=0.97, wspace=0.0, hspace=0.398)
fig.tight_layout()
return fig
[docs]def plot_fit_output( re, ds, cmap = standard_map, plotting = range(6), title = None, path = None, filename = None, f = 'standard',
intensity_range = 1e-2, baseunit = 'ps', timelimits = None, scattercut = None, bordercut = None,
error_matrix_amplification = 20, wave_nm_bin = 5, rel_wave = None, width = 10, rel_time = [1, 5, 10],
time_width_percent = 10, ignore_time_region = None, save_figures_to_folder = True, log_fit = False, mod = None,
subplot = False, color_offset = 0, log_scale = True, savetype = 'png', evaluation_style = False, lintresh = 1,
scale_type = 'symlog', patches = False, print_click_position = False,
data_type = 'differential Absorption in $\mathregular{\Delta OD}$', plot_second_as_energy = True, units = 'nm',
equal_energy_bin = None):
'''Purly manual function that plots all the fit output figures. Quite cumbersome,
but offers a lot of manual options. The figures can be called separately
or with a list of plots. e.g. range(6) call plots 0-5 Manual plotting of certain type:
This is a wrapper function that triggers the plotting of all the fitted plots.
The parameter in this plot call are to control the general look and features of the plot.
Which plots are printed is defined by the command (plotting)
The plots are generated from the fitted Matrixes and as such only will work after a fit was actually
completed (and the "re" dictionary attached to the object.)
In all plots the RAW data is plotted as dots and the fit with lines
*Contents of the plots*
0. DAC contains the assigned spectra for each component of the fit. For
a modelling with independent exponential decays this corresponds to
the "Decay Associated Spectra" (DAS). For all other models this
contains the "Species Associated Spectra" (SAS). According to the
model the separate spectra are labeled by time (process) or name, if
a name is associated in the fitting model. The spectra are shown in
the extracted strength in the right pane and normalized in the left.
Extracted strength means that the measured spectral strength is the
intensity (concentration matrix) times this spectral strength. As the
concentration maxima for all DAS are 1 this corresponds to the
spectral strength for the DAS. (please see the documentation for the
fitting algorithm for further details)
1. summed intensity. All wavelength of the spectral axis are summed for
data and fit. The data is plotted in a number of ways vs linear and
logarithmic axis. This plot is not ment for publication but very
useful to evaluate the quality of a fit.
2. plot kinetics for selected wavelength (see corresponding RAW plot)
3. plot spectra at selected times (see corresponding RAW plot)
4. plots matrix (measured, modelled and error Matrix). The parameter are
the same as used for the corresponding RAW plot with the addition of
"error_matrix_amplification" which is a scaling factor multiplied
onto the error matrix. I recommend to play with different "cmap",
"log_scale" and "intensity_scale" to create a pleasing plot
5. concentrations. In the progress of the modelling/fitting a matrix is
generated that contains the relative concentrations of the species
modelled. This plot is showing the temporal development of these
species. Further details on how this matrix is generated can be found
in the documentation of the fitting function. The modeled spectra are
the convolution of these vectors (giving the time-development) and
the DAS/SAS (giving the spectral development).
Parameters
---------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
re : dict
Dictionary that contains the fit results and specific the dataframes A, AC and AE
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
error_matrix_amplification : int, optional
the error matrix AE is multiplied by this factor for the plot.
plotting : int or iterable (of integers), optional
This parameter determines which figures are plotted
the figures can be called separately with plotting = 1
or with a list of plots (Default) e.g.~plotting=range(6) calls plots 0,1,2,3,4,5
The plots have the following numbers:\
0 - DAS or SAS\
1 - summed intensity\
2 - Kinetics\
3 - Spectra\
4 - Matrixes\
5 - Concentrations (the c-object)\
The plotting takes all parameter from the "ta" object unless otherwise specified
path : None, str or path object, optional
This defines where the files are saved if the safe_figures_to_folder parameter is True,
quite useful if a lot of data sets are to be printed fast.
If a path is given, this is used. If a string like the (Default) "result_figures" is given,
then a subfolder of this name will be used (an generated if necessary)
relative to self.path. Use and empty string to use the self.path
If set to None, the location of the plot_func will be used and
a subfolder with title "result_figures" be generated here
savetype : str or iterable (of str), optional
matplotlib allows the saving of figures in various formats. (Default) "png",
typical and recommendable options are "svg" and "pdf".
evaluation_style : bool, optional
True (Default = False) adds a lot of extra information in the plot
title : None or str, optional
"title=None" is in general the filename that was loaded. Setting a
specific title will be used in all plots. To remove the title all
together set an empty string with title=""
scale_type : str, optional
refers to the time-axis and takes, ’symlog’ (Default)(linear around zero and logarithmic otherwise)
and ‘lin’ for linear and ‘log’ for logarithmic, switching all the time axis to this type
patches : bool, optional
If False (Default) the names "measured" "fitted" "difference" will be placed above the images.
If True, then they will be included into the image (denser)
filename : str, optional
offers to replace the base-name used for all plots (to e.g.~specify what sample was used).
if (Default) None is used, the self.filename is used as a base name. The filename plays only a
role during saving, as does the path and savetype
save_figures_to_folder : bool, optional
(Default) is True, if True the Figures are automatically saved
log_scale : bool, optional
If True (Default), The 2D plots (Matrix) is plotted with a pseudo logarithmic intensity scale.
This usually does not give good results unless the intensity scale is symmetric
subplot : bool, optional
If False (Default) axis labels and such are set. If True, we plot into the same axis and
do not set labels
color_offset : int, optional
At the (Default) 0 the colours are chose from the beginning, for a larger value Color_offset
colors are skipped. Usually only used if multiple plots are created, and the data/or fit is
only shown for some of them.
lintresh : float
The pseudo logratihmic range "symlog" is used for most time axis. Symlog plots a range around
time zero linear and beyond this linear treshold 'lintresh' on a logarithmic scale. (Default) 1
rel_time : float or list/vector (of floats), optional
For each entry in rel_time a spectrum is plotted. If time_width_percent=0 (Default) the
nearest measured timepoint is chosen. For other values see 'time_width_percent'
time_width_percent : float
"rel_time" and "time_width_percent" work together for creating spectral plots at
specific timepoints. For each entry in rel_time a spectrum is plotted.
If however e.g. time_width_percent=10 the region between the timepoint closest
to the 1.1 x timepoint and 0.9 x timepoint is averaged and shown
(and the legend adjusted accordingly). This is particularly useful for the densly
sampled region close to t=0. Typically for a logarithmic recorded kinetics, the
timepoints at later times will be further appart than 10 percent of the value,
but this allows to elegantly combine values around time=0 for better statistics.
This averaging is only applied for the plotting function and not for the fits.
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
width : float, optional
the width used in kinetics, see below (Default) 10nm
rel_wave : float or list (of floats), optional
'rel_wave' and 'width' (in the object called 'wavelength_bin' work together for the creation
of kinetic plots. When plotting kinetic spectra one line will be plotted for each entrance
in the list/vector rel_wave. During object generation the vector np.arange(300,1000,100)
is set as standard. Another typical using style would be to define a list of interesting
wavelength at which a kinetic development is to be plotted. At each selected wavelength
the data between wavelength+ta.wavelength_bin and wavelength-ta.wavelength_bin is averaged
for each timepoint returned
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
equal_energy_bin : None or float(optional)
if this is set the wave_nm_bin is ignored and the data is rebinned into equal energy bins (based upon that the data is in nm.
If dual axis is on then the lower axis is energy and the upper is wavelength
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
f : str
f is a replacement title that is set instead of the title. mainly used to have some options
(Default) is 'standard'
log_fit : bool, optional
(default)= False Used for legend generation, tells if the fit was in log or lin space
mod : str, optional
Used for legend generation, tells what model was used for fitting
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
print_click_position : bool, optional
if True then the click position is printed for the spectral plots
Examples
------------
>>> ta.plot_fit_output(ta.re,ta.ds)
'''
if baseunit != 'ps':
if baseunit == 'ns':baseunit = 'Time in ns'
re['A'].index.name=baseunit
re['AC'].index.name=baseunit
re['AE'].index.name=baseunit
ds.index.name=baseunit
re['c'].index.name=baseunit
if width is None:width=wave_nm_bin
stringen=[]
timedf=re['fit_results_times']
if mod is not None:
if not isinstance(mod,type('hello')):mod='ext. func.'
if evaluation_style:
if mod is not None:
stringen.append('Fit with Model: %s'%mod)
timedf.rename(index={'resolution': "res"},inplace=True)
timedf.rename(columns={'init_value': "init"},inplace=True)
try:
stringen.append(timedf.to_string(columns = ['value','init','vary','min','max','expr'],
float_format = '{:.3g}'.format, justify = 'center'))
except:
print('something strange happened, most likely one value went "inf" or is set unexpectedly to None')
else:
if mod is not None:
if mod in ['paral','exponential']:stringen.append('Fit with ind.\nexpon. decays:')
else:stringen.append('Fit with time parameters:')
try:
timedf.drop(index=['resolution','t0'],inplace=True)
except:
pass
stringen.append(timedf.to_string(columns=['value'],float_format='{:.3g}'.format,header=False))
stringen='\n'.join(stringen)
times=timedf[timedf.is_rate].loc[:,'value'].values
time_string=timedf[timedf.is_rate].to_string(columns=['value'],float_format='{:.3g}'.format,header=False)
if not hasattr(plotting,'__iter__'):plotting=[plotting]
if 0 in plotting:#DAC
#-------plot DAC------------
#for i,col in enumerate(re['DAC']):
#re['DAC'].iloc[:,i]=re['DAC'].iloc[:,i].values*re['c'].max().iloc[i]
fig1,(ax1a,ax1b,ax1c)=plt.subplots(1,3,figsize=(12,5),dpi=100)
n_colors=len(re['DAC'].columns)
DAC=re['DAC']
DAC_copy=DAC.copy()
normed=(DAC/DAC.abs().max())
for i,col in enumerate(DAC_copy):
DAC_copy.iloc[:,i]=DAC_copy.iloc[:,i].values*re['c'].abs().max().iloc[i]
if scattercut is None:
DAC.plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap))
normed.plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap))
DAC_copy.plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap))
elif isinstance(scattercut[0], numbers.Number):
DAC.loc[:scattercut[0],:].plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap))
DAC.loc[scattercut[1]:,:].plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
normed.loc[:scattercut[0],:].plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap))
normed.loc[scattercut[1]:,:].plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
DAC_copy.loc[:scattercut[0],:].plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap))
DAC_copy.loc[scattercut[1]:,:].plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
else:
try:
scattercut=flatten(scattercut)
for i in range(len(scattercut)/2+1):
if i==0:
DAC.loc[:scattercut[0],:].plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap))
normed.loc[:scattercut[0],:].plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap))
DAC_copy.loc[:scattercut[0],:].plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap))
elif i<(len(scattercut)/2):
DAC.loc[scattercut[2*i-1]:scattercut[2*i],:].plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
normed.loc[scattercut[2*i-1]:scattercut[2*i],:].plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
DAC_copy.loc[scattercut[2*i-1]:scattercut[2*i],:].plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
else:
DAC.loc[scattercut[-1]:,:].plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
normed.loc[scattercut[-1]:,:].plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
DAC_copy.loc[scattercut[-1]:,:].plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap), label='_nolegend_')
except:
DAC.plot(ax=ax1b,color=colm(range(n_colors),cmap=cmap))
normed.plot(ax=ax1a,color=colm(range(n_colors),cmap=cmap))
DAC_copy.plot(ax=ax1c,color=colm(range(n_colors),cmap=cmap))
if mod in ['paral','exponential']:
try:
names=['decay %i: %.3g %s'%(i,a,baseunit) for i,a in enumerate(times)]
except:
print('something strange happened, most likely one value went "inf" or is set unexpectedly to None')
names=['decay %i: %s %s'%(i,a,baseunit) for i,a in enumerate(times)]
if 'background' in list(re['DAC'].columns):names.append('background')
if 'Non Decaying' in list(re['DAC'].columns):names.append('Non Decaying')
ax1a.legend(names,title='Model: {}'.format(mod))
ax1b.legend(names,title='Model: {}'.format(mod))
ax1c.legend(names,title='Model: {}'.format(mod))
elif mod in ['exp']:
names=['species %i'%i for i,a in enumerate(re['DAC'].columns.values)]
if 'background' in list(re['DAC'].columns):
if 'Non Decaying' in list(re['DAC'].columns):
names[-1]='background'
names[-2]='Non Decaying'
else:
names[-1]='background'
else:
if 'Non Decaying' in list(re['DAC'].columns):
names[-1]='Non Decaying'
ax1a.legend(names,title='Model: {}'.format(mod))
ax1b.legend(names,title='Model: {}'.format(mod))
ax1c.legend(names,title='Model: {}'.format(mod))
else:
names=['%s'%a for a in re['DAC'].columns.values]
ax1a.legend(names,title='Model: {}'.format(mod))
ax1b.legend(names,title='Model: {}'.format(mod))
ax1c.legend(names,title='Model: {}'.format(mod))
#ax1c.legend(time_string,title='Model: {}'.format(mod))
if title is None:
ax1a.set_title(f)
else:
if len(title)>0:
ax1a.set_title(title)
if title is None:
ax1b.set_title(f)
else:
if len(title)>1:ax1b.set_title(title)
if title is None:
ax1c.set_title(f)
else:
if len(title)>1:ax1c.set_title(title)
ax1a.plot(ax1a.get_xlim(),[0,0],'black',zorder=10)
ax1b.plot(ax1b.get_xlim(),[0,0],'black',zorder=10)
ax1c.plot(ax1b.get_xlim(),[0,0],'black',zorder=10)
ax1a.set_xlabel(ds.columns.name)
ax1b.set_xlabel(ds.columns.name)
ax1c.set_xlabel(ds.columns.name)
ax1a.set_ylabel('intensity norm.')
ax1b.set_ylabel('intensity in arb. units')
ax1c.set_ylabel('intensity*max(c) in arb. units')
fig1.tight_layout()
if 1 in plotting: #-------plot sum_sum------------
fig2 = plt.figure(figsize = (18, 5), dpi = 100)
ax2a=[plt.subplot2grid((3, 3), (0, i)) for i in range(3)]
ax2=[plt.subplot2grid((3, 3), (1, i), rowspan=2) for i in range(3)]
dat = [pandas.DataFrame(re['A'], index = re['A'].index, columns = re['A'].columns).abs().sum(axis = 1)]
dat.append(pandas.DataFrame(re['AC'], index = re['AC'].index, columns = re['AC'].columns).abs().sum(axis = 1))
dat.append(pandas.DataFrame(re['AE'], index = re['AE'].index, columns = re['AE'].columns).abs().sum(axis = 1))
dat_names=['measured','calculated','error']
dat_styles=['*','-','-']
dat_cols=colm(range(3), cmap = cmap)
limits = (dat[0].min(), dat[0].max())
xlimits = (dat[0].index.min(), dat[0].index.max())
if ignore_time_region is None:
for i in range(3):
for j in range(3):
if i==2:
_ = dat[i].plot(ax = ax2a[j], label = dat_names[i], style = dat_styles[i], color = dat_cols[i])
else:
_ = dat[i].plot(ax = ax2[j], label = dat_names[i], style = dat_styles[i], color = dat_cols[i])
elif isinstance(ignore_time_region[0], numbers.Number):
x=dat[0].index.values.astype('float')
lower=find_nearest_index(x,ignore_time_region[0])
upper=find_nearest_index(x,ignore_time_region[1])
for i in range(3):
for j in range(3):
if i==2:
_ = dat[i].iloc[:lower].plot(ax = ax2a[j], label = dat_names[i], style = dat_styles[i], color = dat_cols[i])
_ = dat[i].iloc[upper:].plot(ax = ax2a[j], label = '_nolegend_', style = dat_styles[i], color = dat_cols[i])
else:
_ = dat[i].iloc[:lower].plot(ax = ax2[j], label = dat_names[i], style = dat_styles[i], color = dat_cols[i])
_ = dat[i].iloc[upper:].plot(ax = ax2[j], label = '_nolegend_', style = dat_styles[i], color = dat_cols[i])
else:
try:
ignore_time_region_loc=flatten(ignore_time_region)
for k in range(len(ignore_time_region_loc)/2+1):
if k==0:
for i in range(3):
for j in range(3):
if i==2:
_ = dat[i].loc[:ignore_time_region_loc[k]].plot(ax = ax2a[j], label = dat_names[i], style = dat_styles[i], color = colm(i, cmap = cmap))
else:
_ = dat[i].loc[:ignore_time_region_loc[k]].plot(ax = ax2[j], label = dat_names[i], style = dat_styles[i], color = colm(i, cmap = cmap))
elif k<(len(ignore_time_region)/2):
for i in range(3):
for j in range(3):
if i==2:
_ = dat[i].loc[ignore_time_region_loc[2*k-1]:ignore_time_region_loc[2*k]].plot(ax = ax2a[j], label = '_nolegend_', style = dat_styles[i], color = colm(i, cmap = cmap))
else:
_ = dat[i].loc[ignore_time_region_loc[2*k-1]:ignore_time_region_loc[2*k]].plot(ax = ax2[j], label = '_nolegend_', style = dat_styles[i], color = colm(i, cmap = cmap))
else:
for i in range(3):
for j in range(3):
if i==2:
_ = dat[i].loc[ignore_time_region_loc[-1]:].plot(ax = ax2a[j], label = '_nolegend_', style = dat_styles[i], color = colm(i, cmap = cmap))
else:
_ = dat[i].loc[ignore_time_region_loc[-1]:].plot(ax = ax2[j], label = '_nolegend_', style = dat_styles[i], color = colm(i, cmap = cmap))
except:
for i in range(3):
for j in range(3):
if i==2:
_ = dat[i].plot(ax = ax2a[j], label = dat_names[i], style = dat_styles[i], color = colm(i, cmap = cmap))
else:
_ = dat[i].plot(ax = ax2[j], label = dat_names[i], style = dat_styles[i], color = colm(i, cmap = cmap))
ax2[0].set_xlim(xlimits)
ax2[0].set_xscale('symlog', linscale=0.1)
ax2[0].autoscale(axis='y', tight=True)
ax2a[0].set_xlim(xlimits)
ax2a[0].set_xscale('symlog', linscale=0.1)
ax2a[0].autoscale(axis='y', tight=True)
ax2[1].set_xlim(xlimits)
ax2[1].set_xscale('linear')
ax2[1].set_ylim(np.nanmax([limits[0],limits[1]/10000]),limits[1])
ax2[1].set_yscale('log')
ax2a[1].set_xlim(xlimits)
ax2a[1].set_xscale('linear')
ax2[2].set_xscale('log')
ax2[2].set_ylim(np.nanmax([limits[0],limits[1]/10000]),limits[1])
ax2[2].set_yscale('log')
ax2[2].set_xlim(max(0.1, xlimits[0]), xlimits[1])
ax2a[2].set_xscale('log')
ax2a[2].set_xlim(max(0.1, xlimits[0]), xlimits[1])
#draw a black line at zero
ax2a[0].plot(ax2[0].get_xlim(), [0, 0], 'black', zorder=10, label = '_nolegend_')
ax2a[1].plot(ax2[0].get_xlim(), [0, 0], 'black', zorder=10, label = '_nolegend_')
ax2a[2].plot(ax2[0].get_xlim(), [0, 0], 'black', zorder=10, label = '_nolegend_')
#plot empty to get the labels right
ax2[0].plot([], [], ' ', label=stringen)
ax2[0].legend(title='Model: {}'.format(mod),frameon=False)
if title is None:
ax2[1].legend(labels=[],title=f,frameon=False)
else:
if not len(title)==0:
ax2[1].legend(labels=[],title=title,frameon=False)
for t in times:
if isinstance(t,float):
ax2[0].plot([t,t],ax2[0].get_ylim(),lw=0.5,zorder=10,alpha=0.5,color='black')
ax2[1].plot([t,t],ax2[1].get_ylim(),lw=0.5,zorder=10,alpha=0.5,color='black')
ax2[2].plot([t,t],ax2[2].get_ylim(),lw=0.5,zorder=10,alpha=0.5,color='black')
x_label=ds.index.name
ax2[0].set_xlabel(x_label)
ax2[1].set_xlabel(x_label)
ax2[2].set_xlabel(x_label)
ax2a[0].set_xlabel(x_label)
ax2a[1].set_xlabel(x_label)
ax2a[2].set_xlabel(x_label)
fig2.tight_layout()
if 2 in plotting:#---plot single wavelength----------
fig3,ax3 = plt.subplots(figsize = (15,6),dpi = 100)
#fig3,ax3 = plt.subplots(figsize = (8,4),dpi = 100)
_=plot1d( ds = re['AC'], cmap = cmap, ax = ax3, width = width, wavelength = rel_wave,
lines_are = 'fitted', plot_type = scale_type, baseunit = baseunit, lintresh = lintresh,
timelimits = timelimits, text_in_legend = time_string, title = '',
ignore_time_region = ignore_time_region, data_type = data_type, units = units, from_fit = True)
_=plot1d( ds = re['A'], cmap = cmap,ax = ax3, subplot = True, width = width,
wavelength = rel_wave,lines_are = 'data', plot_type = scale_type,
baseunit = baseunit , lintresh = lintresh , timelimits = timelimits,
ignore_time_region = ignore_time_region, data_type = data_type, units = units, from_fit = True)
ax3.autoscale(axis = 'y',tight = True)
for t in times:
if isinstance(t, float):
ax3.plot([t, t], ax3.get_ylim(), lw = 0.5, zorder = 10, alpha = 0.5, color = 'black')
fig3.tight_layout()
if 3 in plotting: #---plot at set time----------
fig4, ax4 = plt.subplots(figsize = (15, 6), dpi = 100)
_=plot_time(ds=re['A'],cmap=cmap,ax=ax4,subplot=False, rel_time=rel_time, title=title, baseunit=baseunit,
time_width_percent=time_width_percent, lines_are='data', scattercut=scattercut,
bordercut = bordercut, intensity_range = intensity_range, data_type = data_type,
plot_second_as_energy = plot_second_as_energy, units = units, equal_energy_bin = equal_energy_bin, from_fit = True )
_=plot_time(ds=re['AC'],cmap=cmap,ax=ax4, subplot=False, rel_time=rel_time, title=title,
baseunit=baseunit, time_width_percent=time_width_percent, lines_are='fitted',
color_offset=color_offset, scattercut=scattercut, data_type = data_type,
plot_second_as_energy = plot_second_as_energy, units = units, equal_energy_bin = equal_energy_bin, from_fit = True )
try:
if equal_energy_bin is not None:
bordercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in bordercut]
ax4.set_xlim(bordercut)
except:
pass
fig4.tight_layout()
if 4 in plotting:#---matrix with fit and error, as figures----------
fig5 = plot2d_fit(re, cmap = cmap, intensity_range = intensity_range, baseunit = baseunit,
error_matrix_amplification = error_matrix_amplification, wave_nm_bin = None, equal_energy_bin = equal_energy_bin,
use_images = True, log_scale = log_scale, scale_type = scale_type, patches = patches,
lintresh = lintresh, bordercut = bordercut, ignore_time_region = ignore_time_region,
scattercut = scattercut, timelimits = timelimits, levels = 200, data_type = data_type)
plt.ion()
plt.show()
if 5 in plotting:
fig6=plt.figure(figsize=(12,6))
G = GridSpec(4, 4)
ax6=[]
ax6.append(fig6.add_subplot(G[1:,0]))
ax6.append(fig6.add_subplot(G[1:,1]))
ax6.append(fig6.add_subplot(G[1:,2]))
ax6.append(fig6.add_subplot(G[1:,3]))
ax6.append(fig6.add_subplot(G[0,2:]))
ax6.append(fig6.add_subplot(G[0,0:2]))
n_colors=len(re['c'].columns)
for i in range(4):
ax6[i]=re['c'].plot(ax=ax6[i],color=colm(range(n_colors),cmap=cmap),legend=False)
if re['c'].index.name == 'time':
for i in range(4):
ax6[i].set_xlabel('time in %s'%baseunit)
ax6[1].set_yscale('log')
ax6[1].set_ylim(1e-5,1.1)
ax6[2].set_yscale('log')
ax6[2].set_ylim(1e-5,1.1)
ax6[2].set_xscale('log')
ax6[2].set_xlim(0.05,ax6[2].get_xlim()[1])
ax6[3].set_xscale('log')
ax6[3].set_xlim(0.05,ax6[3].get_xlim()[1])
handles, labels = ax6[3].get_legend_handles_labels()
ax6[4].axis('off')
ax6[5].axis('off')
if title is None:
title=f
else:
if not len(title)==0:
title=title
else:
title=''
if len(handles)<5:
ncol=2
elif len(handles)<7:
ncol=3
else:
ncol=4
leg=ax6[4].legend(handles,labels,loc=3, ncol=ncol,edgecolor=(0,0,0,1),framealpha=1,frameon=True,title=title)
for i in range(6):
ax6[i].set_title('')
ax6[5].text(0,0,'This factor represents the temporal evolution\n of the components in the fit.\nThis time dependent factor multiplied with the \nspectral intensity from the SAS/DAS is re[\"AC\"]',fontsize=float(plt.rcParams['legend.fontsize'])-1)
fig6.tight_layout()
if 6 in plotting:#---matrix with fit and error, as contours----------
fig7 = plot2d_fit(re, cmap = cmap, intensity_range = intensity_range, baseunit = baseunit,
error_matrix_amplification = error_matrix_amplification, wave_nm_bin = wave_nm_bin,
use_images = False, scale_type = scale_type, data_type = data_type)
plt.ion()
plt.show()
if print_click_position:
plt.connect('button_press_event', mouse_move)
if save_figures_to_folder:
if path is None:path=os.path.dirname(os.path.realpath(__file__))
figure_path=check_folder(path=path)
if filename is None:
filename='test.fig'
fi=filename.split('.')[0]
try:
fig1.savefig(check_folder(path=figure_path,filename='%s_DAC.%s'%(fi,savetype)),bbox_inches='tight')
fig2.savefig(check_folder(path=figure_path,filename='%s_SUM.%s'%(fi,savetype)),bbox_inches='tight')
fig3.savefig(check_folder(path=figure_path,filename='%s_SEL.%s'%(fi,savetype)),bbox_inches='tight')
fig4.savefig(check_folder(path=figure_path,filename='%s_SPEC.%s'%(fi,savetype)),bbox_inches='tight')
fig5.savefig(check_folder(path=figure_path,filename='%s_FIG_MAT.%s'%(fi,savetype)),bbox_inches='tight')
fig6.savefig(check_folder(path=figure_path,filename='%s_concentrations.%s'%(fi,savetype)),bbox_inches='tight')
fig7.savefig(check_folder(path=figure_path,filename='%s_CONTOUR.%s'%(fi,savetype)),bbox_inches='tight')
except:
pass
[docs]def plot_raw(ds = None, plotting = range(4), title = None, intensity_range = 1e-2, baseunit = 'ps',
timelimits = None, scattercut = None, bordercut = None, wave_nm_bin = None, width = 10,
rel_wave = np.arange(400, 900, 100), rel_time = [1, 5, 10], time_width_percent = 10,
ignore_time_region = None, time_bin = None, cmap = None, color_offset = 0,
log_scale = True, plot_type = 'symlog', lintresh = 0.3, times = None,
save_figures_to_folder = False, savetype = 'png', path = None, filename = None,
print_click_position = False, data_type = 'differential Absorption in $\mathregular{\Delta OD}$',
plot_second_as_energy = True, units = 'nm', return_plots = False, equal_energy_bin = None):
'''This is the extended plot function, for convenient object based plotting see TA.Plot_RAW
This function plotts of various RAW (non fitted) plots. Based on the DataFrame ds a number of
cuts are created using the shaping parameters explained below.
In all plots the RAW data is plotted as dots and interpolated with lines
(using Savitzky-Golay window=5, order=3 interpolation).
Parameters
---------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
plotting : int or iterable (of integers), optional
This parameter determines which figures are plotted
the figures can be called separately with plotting = 1
or with a list of plots (Default) e.g.plotting=range(4) calls plots 0,1,2,3
The plots have the following numbers:
0. Matrix
1. Kinetics
2. Spectra
3. SVD
title : None or str
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will
be used in all plots. To remove the title all together set an empty string with this command title=""
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) "ps", but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
equal_energy_bin : None or float(optional)
if this is set the wave_nm_bin is ignored and the data is rebinned into equal energy bins (based upon that the data is in nm.
If dual axis is on then the lower axis is energy and the upper is wavelength
width : float, optional
the width used in kinetics, see below (Default) 10nm
rel_wave : float or list (of floats), optional
"rel_wave" and "width" (in the object called "wavelength_bin" work together for the creation
of kinetic plots. When plotting kinetic spectra one line will be plotted for each entrance
in the list/vector rel_wave. During object generation the vector np.arange(300,1000,100)
is set as standard. Another typical using style would be to define a list of interesting
wavelength at which a kinetic development is to be plotted. At each selected wavelength
the data between wavelength+ta.wavelength_bin and wavelength-ta.wavelength_bin is averaged
for each timepoint returned
rel_time : float or list/vector (of floats), optional
For each entry in rel_time a spectrum is plotted. If time_width_percent=0 (Default) the
nearest measured timepoint is chosen. For other values see "time_width_percent"
time_width_percent : float
"rel_time" and "time_width_percent" work together for creating spectral plots at
specific timepoints. For each entry in rel_time a spectrum is plotted.
If however e.g. time_width_percent=10 the region between the timepoint closest
to the 1.1 x timepoint and 0.9 x timepoint is averaged and shown
(and the legend adjusted accordingly). This is particularly useful for the densly
sampled region close to t=0. Typically for a logarithmic recorded kinetics, the
timepoints at later times will be further appart than 10 percent of the value,
but this allows to elegantly combine values around time=0 for better statistics.
This averaging is only applied for the plotting function and not for the fits.
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
time_bin : None or int, optional
is dividing the points on the time-axis in even bins and averages the found values in between.
This is a hard approach that also affects the fits. I do recommend to use this carefully,
it is most useful for modulated data. A better choice for transient absorption that only
affects the kinetics is "time_width_percent"
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g. your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
color_offset : int, optional
At the (Default) 0 the colours are chose from the beginning, for a larger value Color_offset
colors are skipped. Usually only used if multiple plots are created, and the data/or fit is
only shown for some of them.
log_scale : bool, optional
If True (Default), The 2D plots (Matrix) is plotted with a pseudo logarithmic intensity scale.
This usually does not give good results unless the intensity scale is symmetric
Scale_type : None or str
is a general setting that can influences what time axis will be used for the plots.
"symlog" (linear around zero and logarithmic otherwise) "lin" and "log" are valid options.
lintresh : float
The pseudo logratihmic range "symlog" is used for most time axis. Symlog plots a range around
time zero linear and beyond this linear treshold 'lintresh' on a logarithmic scale. (Default) 0.3
times : None or int
are the number of components to be used in the SVD (Default) is None (which is seen as 6)
save_figures_to_folder : bool, optional
(Default) is False, if True the Figures are automatically saved
savetype : str or iterable (of str), optional
matplotlib allows the saving of figures in various formats. (Default) "png",
typical and recommendable options are "svg" and "pdf".
path : None, str or path object, optional
This defines where the files are saved if the safe_figures_to_folder parameter is True,
quite useful if a lot of data sets are to be printed fast.
If a path is given, this is used. If a string like the (Default) "result_figures" is given,
then a subfolder of this name will be used (an generated if necessary)
relative to self.path. Use and empty string to use the self.path
If set to None, the location of the plot_func will be used and
a subfolder with title "result_figures" be generated here
filename : str, optional
offers to replace the base-name used for all plots (to e.g.~specify what sample was used).
if (Default) None is used, the self.filename is used as a base name. The filename plays only a
role during saving, as does the path and savetype
print_click_position : bool, optional
if True then the click position is printed for the spectral plots
return_plots : bool, optional
(Default) False, return is ignoriert. For True a dictionary with the handles to the figures is returned
'''
if ds is None:raise ValueError('We need something to plot!!!')
if baseunit != 'ps':
if baseunit == 'ns':
ds.index.name = 'Time in ns'
else:
ds.index.name=baseunit
if path is None:path=check_folder(path='result_figures',current_path=os.path.dirname(os.path.realpath(__file__)))
if filename is None:filename='standard.sia'
if not hasattr(plotting,'__iter__'):plotting=[plotting]
debug=False
plt.ion()
if 0 in plotting:#MAtrix
fig1 = plot2d(ds = ds, cmap = cmap, ignore_time_region = ignore_time_region, plot_type = plot_type,
baseunit = baseunit, intensity_range = intensity_range, scattercut = scattercut,
bordercut = bordercut, wave_nm_bin = wave_nm_bin, levels = 200, lintresh = lintresh,
timelimits = timelimits, time_bin = time_bin, title = title, log_scale = log_scale,
data_type = data_type, equal_energy_bin = equal_energy_bin)
fig1.tight_layout()
if debug:print('plotted Matrix')
if 1 in plotting:#kinetics
fig2,ax2=plt.subplots(figsize=(10,6),dpi=100)
_ = plot1d(ds = ds, ax = ax2, subplot = True, cmap = cmap, width = width, wavelength = rel_wave,
title = title, lines_are = 'data' , plot_type = plot_type, lintresh = lintresh,
timelimits = timelimits, intensity_range = intensity_range, scattercut = scattercut,
ignore_time_region = ignore_time_region, baseunit = baseunit, data_type = data_type,
units = units)
_ = plot1d(ds = ds, ax = ax2, subplot = False, cmap = cmap, width = width, wavelength = rel_wave,
title = title, lines_are = 'smoothed', plot_type = plot_type, lintresh = lintresh,
timelimits = timelimits, intensity_range = intensity_range, scattercut = scattercut,
ignore_time_region = ignore_time_region, baseunit = baseunit, data_type = data_type,
units = units )
if debug:print('plotted kinetics')
if 2 in plotting:#Spectra
fig3,ax3 = plt.subplots(figsize = (10,6),dpi = 100)
_ = plot_time(ds, subplot = True, ax = ax3, plot_second_as_energy = False, cmap = cmap,
rel_time = rel_time, time_width_percent = time_width_percent, title = title,
baseunit = baseunit, lines_are = 'data' , scattercut = scattercut,
wave_nm_bin = wave_nm_bin, bordercut = bordercut, intensity_range = intensity_range,
ignore_time_region = ignore_time_region, data_type = data_type, units = units,
equal_energy_bin = equal_energy_bin)
if plot_second_as_energy:
_ = plot_time(ds ,subplot = False, ax = ax3, plot_second_as_energy = True, cmap = cmap,
rel_time = rel_time, time_width_percent = time_width_percent, title = title,
baseunit = baseunit, lines_are = 'smoothed', scattercut = scattercut,
wave_nm_bin = wave_nm_bin, bordercut = bordercut, intensity_range = intensity_range,
ignore_time_region = ignore_time_region, data_type = data_type, units = units,
equal_energy_bin = equal_energy_bin)
else:
_ = plot_time(ds ,subplot = False, ax = ax3, plot_second_as_energy = False, cmap = cmap,
rel_time = rel_time, time_width_percent = time_width_percent, title = title,
baseunit = baseunit, lines_are = 'smoothed', scattercut = scattercut,
wave_nm_bin = wave_nm_bin, bordercut = bordercut, intensity_range = intensity_range,
ignore_time_region = ignore_time_region, data_type = data_type, units = units,
equal_energy_bin = equal_energy_bin)
fig3.tight_layout()
if debug:print('plotted Spectra')
if 3 in plotting: #---plot at set time----------
try:
fig4 = SVD(ds , times = times , timelimits = timelimits , scattercut = scattercut ,
bordercut = bordercut , wave_nm_bin = wave_nm_bin, ignore_time_region = ignore_time_region,
cmap = cmap)
except:
print("SVD failed with:",sys.exc_info()[0])
if debug:print('plotted SVD')
if print_click_position:
plt.connect('button_press_event', mouse_move)
plt.show()
if save_figures_to_folder:
fi=filename.split('.')[0]
try:
fig1.savefig(check_folder(path=path,filename='%s_RAW_MAT.%s'%(fi,savetype)),bbox_inches='tight',dpi=300)
fig2.savefig(check_folder(path=path,filename='%s_RAW_SEL.%s'%(fi,savetype)),bbox_inches='tight',dpi=300)
fig3.savefig(check_folder(path=path,filename='%s_RAW_SPEK.%s'%(fi,savetype)),bbox_inches='tight',dpi=300)
fig4.savefig(check_folder(path=path,filename='%s_RAW_SVD.%s'%(fi,savetype)),bbox_inches='tight',dpi=300)
except:
pass
if return_plots:
return_dicten={}
try:
return_dicten[0]=fig1
except:
pass
try:
return_dicten[1]=fig2
except:
pass
try:
return_dicten[2]=fig3
except:
pass
try:
return_dicten[3]=fig4
except:
pass
return return_dicten
[docs]def plot_time(ds, ax = None, rel_time = None, time_width_percent = 10, ignore_time_region = None,
wave_nm_bin = None, title = None, text_in_legend = None, baseunit = 'ps',
lines_are = 'smoothed', scattercut = None, bordercut = None, subplot = False, linewidth = 1,
color_offset = 0, intensity_range = None, plot_second_as_energy = True, cmap = standard_map,
data_type = 'differential Absorption in $\mathregular{\Delta OD}$', units = 'nm', equal_energy_bin = None, from_fit = None):
'''Function to create plots at a certain time. In general you give under rel_time a
list of times at which yu do want to plot the time width percentage means that
this function integrates ewverything plus minus 10% at this time. lines_are is a
switch that regulates what is plotted. data plots the data only,
smoothed plots the data and a smoothed version of the data, fitted plots only the fit.
the subplot switch is for using this to plot e.g. multiple different datasets.
Parameters
---------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
ax : None or matplotlib axis object, optional
if None (Default), a figure and axis will be generated for the plot, if axis is given the plot will
placed in there.
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
rel_time : float or list/vector (of floats), optional
For each entry in rel_time a spectrum is plotted. If time_width_percent=0 (Default) the
nearest measured timepoint is chosen. For other values see 'time_width_percent'
time_width_percent : float
"rel_time" and "time_width_percent" work together for creating spectral plots at
specific timepoints. For each entry in rel_time a spectrum is plotted.
If however e.g. time_width_percent=10 the region between the timepoint closest
to the 1.1 x timepoint and 0.9 x timepoint is averaged and shown
(and the legend adjusted accordingly). This is particularly useful for the densly
sampled region close to t=0. Typically for a logarithmic recorded kinetics, the
timepoints at later times will be further appart than 10 percent of the value,
but this allows to elegantly combine values around time=0 for better statistics.
This averaging is only applied for the plotting function and not for the fits.
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
title : None or str, optional
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will
be used in all plots. To remove the title all together set an empty string with this command title=""
linewidth : float, optional
linewidth to be used for plotting
text_in_legend : str, optional
text to be used in legend before the actually lines and colours (set as heasder)
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
subplot ; bool, optional
False (Default) means this is a main plot in this axis! if True then this is the second plot in the axis
and things like axis ticks should not be reset
this also avoids adding the object to the legend
color_offset : int, optional
At the (Default) 0 the colours are chose from the beginning, for a larger value Color_offset
colors are skipped. Usually only used if multiple plots are created, and the data/or fit is
only shown for some of them.
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
plot_second_as_energy : bool, optional
For (Default) True a second x-axis is plotted with "eV" as unit
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
'''
if not hasattr(rel_time,'__iter__'):rel_time=[rel_time]
rel_time=[a for a in rel_time if a<ds.index.values.astype('float').max()]
if isinstance(cmap,list):
colors=cmap[color_offset:]
else:
colors=colm(np.arange(color_offset,len(rel_time)+color_offset),cmap=cmap)
if ax is None:
fig,ax1=plt.subplots(figsize=(10,6),dpi=100)
else:
ax1=ax
ds = sub_ds(ds = ds, times = rel_time, time_width_percent = time_width_percent,
scattercut = scattercut, drop_scatter=True, bordercut = bordercut, baseunit=baseunit,
ignore_time_region = ignore_time_region, wave_nm_bin = wave_nm_bin, equal_energy_bin = equal_energy_bin, from_fit = from_fit)
if 'smoothed' in lines_are:
if scattercut is None:
smoothed=Frame_golay(ds, window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
elif isinstance(scattercut[0], numbers.Number):#handling single scattercut
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
scattercut=scattercut[::-1]
smoothed=Frame_golay(ds.loc[:scattercut[0],:], window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
smoothed=Frame_golay(ds.loc[scattercut[1]:,:], window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
else:#handling multiple scattercuts
try:
scattercut=flatten(scattercut)
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
scattercut=scattercut[::-1]
for i in range(len(scattercut)):
if i==0:
smoothed=Frame_golay(ds.loc[:scattercut[0],:], window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
elif i<(len(scattercut)/2):
smoothed=Frame_golay(ds.loc[scattercut[2*i-1]:scattercut[2*i],:], window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
else:
smoothed=Frame_golay(ds.loc[scattercut[-1]:,:], window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
except:
print('printing the smoothed scatter interpolation created an error, using default')
smoothed=Frame_golay(window = 5, order = 3,transpose=False)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
if not subplot:
leg = ax1.legend(ds,title = 'lines = smoothed', loc='best', labelspacing = 0, ncol = 2, columnspacing = 1, handlelength = 1, frameon = False)
elif 'data' in lines_are:
if subplot:
ax1 = ds.plot(ax = ax1, legend = False, style = '*', color = colors, zorder = 0)
else:
ax1 = ds.plot(ax = ax1, legend = False, style = '*', color = colors)
leg = ax1.legend(ds,labelspacing = 0, ncol = 2, columnspacing = 1, handlelength = 1, loc = 'best', frameon = False)
elif 'fitted' in lines_are:
if scattercut is None:
ds.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, alpha = 0.7)
elif isinstance(scattercut[0], numbers.Number):
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
ds.loc[:scattercut[0],:].plot(ax = ax1, legend = False, style = '-', color = colors, alpha = 0.7, lw = linewidth)
ds.loc[scattercut[1]:,:].plot(ax = ax1, legend = False, style = '-', color = colors, alpha = 0.7, lw = linewidth, label='_nolegend_')
else:
try:
scattercut=flatten(scattercut)
if equal_energy_bin is not None:
scattercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in scattercut]
for i in range(len(scattercut)):
if i==0:
ds.loc[:scattercut[0],:].plot(ax = ax1, legend = False, style = '-', color = colors, alpha = 0.7, lw = linewidth)
elif i<(len(scattercut)/2):
ds.loc[scattercut[2*i-1]:scattercut[2*i],:].plot(ax = ax1, legend = False, style = '-', color = colors, alpha = 0.7, lw = linewidth, label='_nolegend_')
else:
ds.loc[scattercut[-1]:,:].plot(ax = ax1, legend = False, style = '-', color = colors, alpha = 0.7, lw = linewidth, label='_nolegend_')
except:
ds.plot(ax = ax1, legend = False, style = '-', color = colors, alpha = 0.7, lw = linewidth)
if not subplot:leg = ax1.legend(ds,title = 'lines = fit', loc = 'best', labelspacing = 0, ncol = 2, columnspacing = 1, handlelength = 1, frameon = False)
if not subplot:
if text_in_legend is not None:
stringen=leg.get_title().get_text()
texten=text_in_legend
leg.set_title(texten + '\n' +stringen)
else:#for multiple plotting
return ax1
if bordercut is None:
ax1.autoscale(axis='x',tight=True)
else:
if equal_energy_bin is not None:
bordercut=[scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt) for a in bordercut]
#bordercut=bordercut[::-1]
ax1.set_xlim(bordercut)
if (not subplot) and plot_second_as_energy:
ax2=ax1.twiny()
ax2.set_xlim(ax1.get_xlim())
ax2.set_xticks(ax1.get_xticks())
if equal_energy_bin is not None:
labels=['%.1f'%(scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt)) for a in ax2.get_xticks()]
else:
labels=['%.2f'%(scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt)) for a in ax2.get_xticks()]
_=ax2.set_xticklabels(labels)
if equal_energy_bin is not None:
_=ax2.set_xlabel('Wavelength in nm')
else:
_=ax2.set_xlabel('Energy in eV')
ax1.set_zorder(ax2.get_zorder()+1)
if not subplot:
if not len(title)==0:
try:
ax2.set_title(title,pad=10)
except:
ax1.set_title(title,pad=10)
ax1.set_ylabel(data_type)
ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: '%.2g'%(x)))
ax1.set_xlabel(ds.index.name)
ax1.minorticks_on()
#ax1.xaxis.set_minor_locator(AutoMinorLocator(6))
ax1.plot(ax1.get_xlim(),[0,0],color='black',lw=0.5,zorder=0, label='_nolegend_')
if intensity_range is None:
ax1.autoscale(axis='y',tight=True)
else:
if not hasattr(intensity_range,'__iter__'):#lets have an lazy option
intensity_range=np.array([-intensity_range,intensity_range])
ax1.set_ylim(intensity_range)
if ax is None:
return fig
else:
return ax1
[docs]def plot1d(ds = None, wavelength = None, width = None, ax = None, subplot = False, title = None, intensity_range = None,
baseunit = 'ps', timelimits = None, scattercut = None, bordercut = None, cmap = standard_map, plot_type = 'symlog',
lintresh = 0.1, text_in_legend = None, lines_are = 'smoothed', color_offset = 0, ignore_time_region = None,
linewidth = 1, data_type = 'differential Absorption in $\mathregular{\Delta OD}$', units = 'nm', from_fit = False):
'''Plots the single line kinetic for specific wavelength given with the parameter wavelength.
Parameters
---------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
wavelength : float or list (of floats)
wavelength is in the object called "rel_wave" and works with "width"
(in the object called "wavelength_bin") together for the creation
of kinetic plots. When plotting kinetic spectra one line will be plotted for each entrance
in the list/vector rel_wave. During object generation the vector np.arange(300,1000,100)
is set as standard. Another typical using style would be to define a list of interesting
wavelength at which a kinetic development is to be plotted. At each selected wavelength
the data between wavelength+ta.wavelength_bin and wavelength-ta.wavelength_bin is averaged
for each timepoint returned
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
width : float, optional
the width used in kinetics, see below (Default) 10nm
ax : None, matplotlib axis object optional
If None (Default) a new plot is is created and a new axis, otherwise ax needs to be Matplotlib Axis
subplot : bool, optional
If False (Default) axis labels and such are set. If True, we plot into the same axis and
do not set labels
text_in_legend : None, str, optional
extra text to be put into the legend (above the lines)
lines_are : str, optional
Depending on this parameter the plots contain:
'smoothed' = data lines of golay smoothed data (Default)
'data' = dots are data,
'fitted' = not data, just lines shown
title : None or str
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will
be used in all plots. To remove the title all together set an empty string
linewidth : float, optional
linewidht to be used for plotting
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
color_offset : int, optional
At the (Default) 0 the colours are chose from the beginning, for a larger value Color_offset
colors are skipped. Usually only used if multiple plots are created, and the data/or fit is
only shown for some of them.
plot_type : None or str
is a general setting that can influences what time axis will be used for the plots.
"symlog" (linear around zero and logarithmic otherwise) "lin" and "log" are valid options.
lintresh : float
The pseudo logratihmic range "symlog" is used for most time axis. Symlog plots a range around
time zero linear and beyond this linear treshold 'lintresh' on a logarithmic scale. (Default) 0.3
from_fit : bool optional
i needed this swtich to avoid re-slicing of data in spectal axis for equal energy bins
'''
if not isinstance(ds,pandas.DataFrame):
print("input format wrong")
if ax is None:
fig,ax1=plt.subplots(figsize=(10,6),dpi=100)
else:
ax1=ax
if width is None:width=1
if not hasattr(wavelength, '__iter__'):wavelength = [wavelength]
if isinstance(cmap,list):
colors=cmap[color_offset:]
else:
colors = colm(np.arange(color_offset, len(wavelength)+color_offset), cmap = cmap)
ds = sub_ds(ds = ds, wavelength = wavelength, wavelength_bin = width, scattercut = scattercut,
ignore_time_region = ignore_time_region, drop_ignore = True, from_fit = from_fit)
if 'smoothed' in lines_are:
if ignore_time_region is None:
smoothed=Frame_golay(ds, window = 5, order = 3)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
elif isinstance(ignore_time_region[0], numbers.Number):
smoothed=Frame_golay(ds.loc[:ignore_time_region[0],:], window = 5, order = 3)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
smoothed=Frame_golay(ds.loc[ignore_time_region[1]:,:], window = 5, order = 3)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
else:
try:
ignore_time_region=flatten(ignore_time_region)
for i in range(len(ignore_time_region)/2+1):
if i==0:
smoothed=Frame_golay(ds.loc[:ignore_time_region[0],:], window = 5, order = 3,transpose=True)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
elif i<(len(ignore_time_region)/2):
smoothed=Frame_golay(ds.loc[ignore_time_region[2*i-1]:ignore_time_region[2*i],:], window = 5, order = 3,transpose=True)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
else:
smoothed=Frame_golay(ds.loc[ignore_time_region[-1]:,:], window = 5, order = 3,transpose=True)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
except:
smoothed=Frame_golay(ds, window = 5, order = 3)
smoothed.plot(ax = ax1, style = '-', color = colors, legend = False, lw = linewidth)
elif 'data' in lines_are:
if subplot:ds.plot(ax = ax1, style = '*', color = colors, legend = False, zorder = 0, label='_nolegend_')
else: ds.plot(ax = ax1, style = '*', color = colors, legend = False)
elif 'fitted' in lines_are:
if ignore_time_region is None:
ds.plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth)
elif isinstance(ignore_time_region[0], numbers.Number):
ds.loc[:ignore_time_region[0],:].plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth)
ds.loc[ignore_time_region[1]:,:].plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
else:
try:
ignore_time_region=flatten(ignore_time_region)
for i in range(len(ignore_time_region)/2+1):
if i==0:
ds.loc[:ignore_time_region[0],:].plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth)
elif i<(len(ignore_time_region)/2):
ds.loc[ignore_time_region[2*i-1]:ignore_time_region[2*i],:].plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
else:
ds.loc[ignore_time_region[-1]:,:].plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth, label='_nolegend_')
except:
ds.plot(ax = ax1, style='-', color = colors, legend = False, lw = linewidth)
#Legend
if not subplot:
handles, labels = ax1.get_legend_handles_labels()
handles=handles[:len(wavelength)]
labels=labels[:len(wavelength)]
for i,entry in enumerate(labels):
labels[i]=entry + ' %s'%units
if 'smoothed' in lines_are:leg=ax1.legend(labels,title='%g %s width, lines=smoothed'%(width,units),labelspacing=0,ncol=2,columnspacing=1,handlelength=1,frameon=False)
elif 'data' in lines_are: leg=ax1.legend(labels,title='%g %s width'%(width,units) ,labelspacing=0,ncol=2,columnspacing=1,handlelength=1,frameon=False)
elif 'fitted' in lines_are:leg=ax1.legend(labels,title='%g %s width, lines=fit'%(width,units) ,labelspacing=0,ncol=2,columnspacing=1,handlelength=1,frameon=False)
if text_in_legend is not None:
stringen=leg.get_title().get_text()
texten=text_in_legend
leg.set_title(texten + '\n' +stringen)
x=ds.index.values.astype('float')
#limits and ticks
if timelimits is None:timelimits=[min(x),max(x)]
if "symlog" in plot_type:
lintresh=lintresh
ax1.set_xscale('symlog', linthresh=lintresh,subs=range(2,9),linscale=lintresh/4.)
try:
if lintresh>0.5:
ticks=np.concatenate((np.arange(-100,0,10,),[-5,-3,-2,-1,-0.5,0,0.5],np.logspace(0,10,11)))
elif lintresh>=0.3:
ticks=np.concatenate((np.arange(-100,0,10,),[-5,-3,-2,-1,-0.3,0,0.3],np.logspace(0,10,11)))
elif lintresh>=0.1:
ticks=np.concatenate((np.arange(-100,0,10,),[-5,-3,-2,-1,-0.1,0,0.1],np.logspace(0,10,11)))
else:
ticks=np.concatenate((np.arange(-100,0,10,),[-5,-3,-2,-1,0],np.logspace(0,10,11)))
ticks=ticks[ticks>timelimits[0]]
ticks=ticks[ticks<timelimits[1]]
ax1.set_xticks(ticks)
except:
pass
ax1.set_xlim(timelimits[0],timelimits[1])
elif "log" in plot_type:
lower_time=max(1e-6,timelimits[0])
ax1.set_xlim(lower_time,timelimits[1])
ax1.set_xscale('log')
elif "lin" in plot_type:
ax1.set_xlim(timelimits[0],timelimits[1])
if intensity_range is None:
ax1.autoscale(axis='y',tight=True)
else:
if not hasattr(intensity_range,'__iter__'):#lets have an lazy option
intensity_range=np.array([-intensity_range,intensity_range])
ax1.set_ylim(intensity_range)
if not subplot:
ax1.plot(ax1.get_xlim(),[0,0],'black',lw=1,zorder=10, label='_nolegend_')
if title is not None:
if title:
try:
ax2.set_title(title,pad=10)
except:
ax1.set_title(title,pad=10)
if "symlog" in plot_type:
ax1.plot([lintresh,lintresh],ax1.get_ylim(),color='black',linestyle='dashed',alpha=0.5)
ax1.plot([-lintresh,-lintresh],ax1.get_ylim(),color='black',linestyle='dashed',alpha=0.5)
ax1.set_xlabel(ds.index.name)
ax1.set_ylabel(data_type)
#ax1.set_xlabel('time in %s'%baseunit)
#ax1.set_ylabel('differential Absorption in $\mathregular{\Delta OD}$')
ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, pos: '%.2g'%(x)))
if ax is None:
return fig
else:
return ax1
[docs]def SVD(ds, times = None, scattercut = None, bordercut = None, timelimits = [5e-1, 150], wave_nm_bin = 10,
time_bin = None, wavelength_bin = None, plotting = True, baseunit = 'ps', title = None, ignore_time_region = None,
cmap = None, equal_energy_bin = None, data_type = 'differential Absorption in $\mathregular{\Delta OD}$'):
'''This function calculates the SVD and plots an overview.
Parameters
------------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
times : None or int
are the number of components to be used in the SVD (Default) is None (which is seen as 6)
plotting : bool
if True (Default) the functions plots the SVD, if False it returns the vectors
title : None or str
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will
be used in all plots. To remove the title all together set an empty string with this command title=""
baseunit : str
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
timelimits : None or list (of 2 floats), optional
cut times at the low and high time limit. (Default) [5e-1 , 150] uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
bordercut : None or iterable (with two floats), optional
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared. (Default = 10)
wavelength_bin : float, optional
the width used in kinetics, see below (Default) 10nm
ignore_time_region : None or list (of two floats or of lists), optional
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
time_bin : None or int, optional
is dividing the points on the time-axis in even bins and averages the found values in between.
This is a hard approach that also affects the fits. I do recommend to use this carefully,
it is most useful for modulated data. A better choice for transient absorption that only
affects the kinetics is 'time_width_percent'
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
'''
if times is None:
max_order=6
else:
max_order=times
if cmap is None:cmap=standard_map
colors=colm(np.arange(0,max_order,1),cmap=cmap)
ds = sub_ds(ds, scattercut = scattercut, bordercut = bordercut, timelimits = timelimits, wave_nm_bin = wave_nm_bin,
wavelength_bin = wavelength_bin, time_bin = time_bin, ignore_time_region = ignore_time_region)
U, s, V = np.linalg.svd(ds.values)
if plotting:
fig=plt.figure(figsize=(8,8),dpi=100)
G = GridSpec(2, 6)
ax1=fig.add_subplot(G[0,:2])
ax2=fig.add_subplot(G[1,:])
ax3=fig.add_subplot(G[0,2:])
if title is not None:
ax2.set_title(title,fontsize=plt.rcParams['figure.titlesize']-4)
else:
ax1.set_title("Component\nstrength",fontsize=plt.rcParams['figure.titlesize']-4)
ax2.set_title("Spectral component",fontsize=plt.rcParams['figure.titlesize']-4)
ax3.set_title("Temporal development\nof spectral component",fontsize=plt.rcParams['figure.titlesize']-4)
s/=s.max()
ax1.scatter(np.arange(max_order)+1,s[:max_order],c=colors,s=100)
ax1.set_xlabel('SVD order',fontsize=plt.rcParams['axes.labelsize']-2)
ax1.set_ylabel('Singular values norm.',fontsize=plt.rcParams['axes.labelsize']-2)
ax1.set_xlim(0.5,max_order+0.5)
if max_order == 6:
ax1.set_xticks([round(a) for a in np.linspace(1,max_order,6)])
else:
ax1.set_xticks([round(a) for a in np.linspace(1,max_order,5)])
V2=pandas.DataFrame(V.T,index=ds.columns.values.astype('float'))
U2=pandas.DataFrame(U,index=ds.index.values.astype('float'))
U2=U2.iloc[:,:len(s)].multiply(-s)
V2=V2.iloc[:,:len(s)].multiply(-s)
names=['SVD vector %i'%(a+1) for a in range(max_order)]
U2=U2.iloc[:,:max_order]
U2.columns=names
V2=V2.iloc[:,:max_order]
V2.columns=names
V2/=V2.abs().max(axis=1).max()
V2.plot(ax=ax2,color=colors)
ax2.set_ylabel('Intensity norm.',fontsize=plt.rcParams['axes.labelsize']-2)
U2/=U2.abs().max(axis=1).max()
U2.plot(ax=ax3,color=colors)
ax3.set_ylabel('Intensity norm.',fontsize=plt.rcParams['axes.labelsize']-2)
ax2.set_xlabel(ds.columns.name,fontsize=plt.rcParams['axes.labelsize']-2)
lims=V2.index.values.astype(float)
ax2.set_xlim(lims.min(),lims.max())
#ax2.set_xticks(np.linspace(round(lims.min(),-2),round(lims.max()-2),5))
ax2.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: '%.4g'%(x)))
ax3.set_xlabel(ds.index.name,fontsize=plt.rcParams['axes.labelsize']-2)
tims=U2.index.values.astype(float)
ax3.set_xlim(max([0.01,tims.min()]),tims.max())
ax3.set_xscale('log')
#ax3.set_xticks(np.logspace(-1,2,round(np.log())))
ax3.xaxis.set_major_formatter(FuncFormatter(lambda x, pos: '%1g'%(x)))
ax2.legend(frameon=False,labelspacing=0,borderpad=0,numpoints=1,handlelength=1)
ax3.legend(frameon=False,fontsize=11,labelspacing=0,borderpad=0,numpoints=1,handlelength=1)
fig.tight_layout()
return fig
else:
return U, s, V2,ds
[docs]def Species_Spectra(ta=None,conc=None,das=None):
'''useful help function that returns a dictionary that has DataFrame as entries and the names of the
components as keys
Parameters
-----------
ta : plot_func.TA object, optional
This object should contain a successful fit. The function will cycle through the fitted species
and return the matrix that is formed from the dynamics and the species associated spectrum
If this given, then "conc" and "das" are ignored. We cycle through the columns of the concentration
and take the same column from the das Frame.
conc : DataFrame, optional
Is read only if ta_object is None. This should contain the concentration matrix with the species as
as columns
das : DataFrame, optional
This should contain the spectra of the species with one column per spectrum. The position of the columns
must match the columns in the conc (at least this is what is assumed)
Examples
---------
dicten=Species_Spectra(ta)
'''
if ta is not None:
try:
time=ta.re['c'].index.values
WL=ta.re['DAC'].index.values
conc=ta.re['c']
das=ta.re['DAC']
except:
print('the TA object must contain a successful fit')
print(ta.re)
return False
else:
if (conc is None) or (das is None):
print('If the ta object is None, then we need both the conc and the das')
return False
else:
time=conc.index.values
WL=das.index.values
results={}
for i in range(len(conc.columns)):
A,B=np.meshgrid(conc.iloc[:,i].values,das.iloc[:,i].values)
C=pandas.DataFrame((A*B).T,index=time,columns=WL)
results[conc.columns[i]]=C
try:
for key in results.keys():
results[key].index.name=ta.re['c'].index.name
results[key].columns.name=ta.re['DAC'].index.name
except Exception as e:
print(e)
return results
def Fix_Chirp(ds, save_file = None, scattercut = None, intensity_range = 5e-3, wave_nm_bin = 10, bordercut=None,
shown_window = [-1.5, 1.5], filename = None, path = None, fitcoeff = None, max_points = 40,
cmap = cm.prism):
'''Manual selecting polynom for chirp. This function is opening
a plot and allows the user to select a number of points, which are then
approximated with a 4th order polynomial and finally to select a point
that is declared as time zero. The observed window as well as the intensities
and the colour map can be chosen to enable a good correction. Here a fast
iterating colour scheme such as "prism" is often a good choice. In all of the
selections a left click selects, a right click removes the last point and
a middle click (sometime appreviated by clicking left and right together)
finishes the selection. If no middle click exists, the process
automatically ends after max_points (40 preset).
Many of the parameters are from the raw plotting part
Parameters
-----------
ds : DataFrame
This dataframe contains the data to be plotted. It is copied and sliced into the
regions defined. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
save_file : None or str, optional
If a raw file was read(e.g. "data.SIA") and the chirp correction was
completed, a file with the attached word "chirp" is created and
stored in the same location. ("data_chirp.dat") This file contains
the 5 values of the chirp correction. By selecting such a file
(e.g. from another raw data) a specific chirp is applied. If a
specific name is given with **chirp_file** (and optional **path**)
then this file is used.\n
GUI\n
The word *'gui'* can be used instead of a filename to open a gui that
allows the selection of a chrip file
scattercut : None or iterable (of floats or other iterable, always pairs!), optional
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
intensity_range : None, float or list [of two floats]
intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
wave_nm_bin : None or float, optional
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
shown_window : list (with two floats), optional
Defines the window that is shown during chirp correction. If the t=0 is not visible, adjust this parameter
to suit the experiment. If problems arise, I recomment to use Plot_Raw to check where t=0 is located
filename : str, optional
name of the original file, that will be used to save the results later (with attached "_chirp")
path : str or path object (optional)
if path is a string without the operation system dependent separator, it is treated as a relative path,
e.g. data will look from the working directory in the sub director data. Otherwise this has to be a
full path in either strong or path object form.
fitcoeff : list or vector (5 floats), optional
One can give a vector/list with 5 numbers representing the parameter
of a 4th order polynomial (in the order
:math:`(a4*x^4 + a3*x^3+a2*x^2+a1*x1+a0)`. The chirp parameter are
stored in ta.fitcoeff and can thus be used in other TA objects. This
vector is also stored with the file and automatically applied during
re-loading of a hdf5-object
max_points : int, optional
Default = 40 max numbers of points to use in Gui selection. Useful option in case no middle mouse button
is available. (e.g. touchpad)
cmap : matplotlib colourmap, optional
Colourmap to be used for the chirp correction. While there is a large selection here I recommend to choose
a different map than is used for the normal 2d plotting.\n
cm.prism (Default) has proofen to be very usefull
'''
ds=ds.fillna(0)
if fitcoeff is not None:#we loaded a previous project this is a dublication, but I'm currently to lazy to make this tighter
if isinstance(fitcoeff,str):fitcoeff=np.array(fitcoeff.split(','),dtype='float')
if len(fitcoeff)==6:#old style parameter
fitcoeff[-2]-=fitcoeff[-1]
fitcoeff=fitcoeff[:5]
wl=ds.columns.values.astype('float')#extract the wavelength
time=ds.index.values.astype('float')#extract the time
for i in range(0, len(wl), 1):
correcttimeval = np.polyval(fitcoeff, wl[i])
f = scipy.interpolate.interp1d((time-correcttimeval), ds.values[:,i], bounds_error=False, fill_value=0)
fixed_wave = f(time)
ds.values[:, i] = fixed_wave
return ds
else:
if save_file is None:
#lets start by choosing a good intensity
if hasattr(intensity_range,'__iter__'):
maxim=max(abs(np.array(intensity_range)))
intensity_range=maxim
elif intensity_range is None:
intensity_range=ds.abs().max().max()
intensities=(2**np.arange(-6.,4,1.))*intensity_range
window_difference=np.abs(shown_window[1]-shown_window[0])*0.1
timelimits=shown_window+np.asarray([-window_difference,window_difference])
for repeat in range(30):
fig,ax=plt.subplots()
ax = plot2d(ax = ax, cmap = cmap, ds = ds, wave_nm_bin = wave_nm_bin, scattercut = scattercut, bordercut = bordercut,
timelimits = timelimits, intensity_range = intensity_range,
title = 'select intensity where we can work,\n if happy, choose confirm or abort',
use_colorbar = False, plot_type = "linear", log_scale = False)
w=(ax.get_xlim()[1]-ax.get_xlim()[0])
ax.add_patch(matplotlib.patches.Rectangle((ax.get_xlim()[0],shown_window[1]),w,0.5,facecolor='white'))
for i,ent in enumerate(intensities):
ax.text(ax.get_xlim()[0]+i*w/10.,shown_window[1],'%.1g'%(2**np.arange(-6.,4,1.))[i],fontsize=8)
ax.add_patch( matplotlib.patches.Rectangle((ax.get_xlim()[0],timelimits[0]),w/4.,0.2,facecolor='white'))
ax.text(ax.get_xlim()[0],timelimits[0],'Accept',fontsize=15)
ax.add_patch(matplotlib.patches.Rectangle((ax.get_xlim()[0]+w*3./4.,timelimits[0]),w/4.,0.2,facecolor='white'))
ax.text(ax.get_xlim()[0]+w*3./4.,timelimits[0],'Cancel all',fontsize=15)
choice =plt.ginput(1,timeout=15)
factor=int((choice[0][0]-ax.get_xlim()[0])/(w/10.))
if choice[0][1] > shown_window[1]/2:
intensity_range=intensities[factor]
print((2**np.arange(-6.,4,1.))[factor])
intensity_range=[-intensity_range,intensity_range]
plt.close(fig)
continue
elif choice[0][1] < shown_window[0]/2.:#we choice to finish the choices
if choice[0][0] < ax.get_xlim()[0]+w/2:#
print('accept')
plt.close(fig)
break
else:
plt.close(fig)
return False
else:
print('click better please')
plt.close(fig)
continue
for repeat in range(10):
fig,ax=plt.subplots()
ax = plot2d(ax = ax, cmap = cmap, ds = ds, wave_nm_bin = wave_nm_bin, scattercut = scattercut, bordercut = bordercut,
timelimits = shown_window, intensity_range = intensity_range,
title = 'select points, rightclick = remove last, \n middle click (or both at once finishes ',
use_colorbar = False, plot_type = "linear", log_scale = False)
polypts=np.asarray(plt.ginput(n=max_points,timeout=300, show_clicks=True,mouse_add=1, mouse_pop=3, mouse_stop=2))
plt.close(fig)
fig,ax=plt.subplots()
ax = plot2d(ax = ax, ds = ds, cmap = cmap, wave_nm_bin = wave_nm_bin, scattercut = scattercut, bordercut = bordercut,
timelimits = shown_window, intensity_range = intensity_range,
title = 'like it? %i more attempts'%(9-repeat), use_colorbar = False,
plot_type = "linear", log_scale = False)
#Fit a polynomial of the form p(x) = p[2] + p[1] + p[0]
fitcoeff= np.polyfit(polypts[:, 0], polypts[:, 1], 4, full=False)
correcttimeval = np.polyval(fitcoeff, ds.columns.values.astype('float'))
ax.plot(ds.columns.values.astype('float'),correcttimeval)
ax.add_patch( matplotlib.patches.Rectangle((ax.get_xlim()[0],ax.get_ylim()[0]),w/4,0.2,facecolor='white'))
ax.text(ax.get_xlim()[0],ax.get_ylim()[0]+0.05,'Save',fontsize=20)
ax.add_patch(matplotlib.patches.Rectangle((ax.get_xlim()[0]+w*3/4,ax.get_ylim()[0]),w/4,0.2,facecolor='white'))
ax.text(ax.get_xlim()[0]+w*3/4,ax.get_ylim()[0]+0.05,'Redo',fontsize=20)
satisfied =plt.ginput(1)
plt.close(fig)
if satisfied[0][0] < ax.get_xlim()[0]+w/2:
print('accepted')
plt.close(fig)
break
elif repeat<8:
plt.close(fig)
continue
else:
plt.close(fig)
return False
#stdev = sum(residuals**2)/8
else:
with open(save_file,'r') as f:
fitcoeff=f.readline()
fitcoeff=np.array(fitcoeff.split(','),dtype='float')
if len(fitcoeff)==6:#old style params
fitcoeff[-2]-=fitcoeff[-1]
fitcoeff=fitcoeff[:5]
time=ds.index.values.astype('float')#extract the time
ds_new=ds.apply(lambda x:np.interp(x=time+np.polyval(fitcoeff,float(x.name)),xp=time,fp=x),axis=0,raw=False)
if save_file is None:
#finding where zero time is
for repeat in range(10):
fig,ax=plt.subplots()
ax = plot2d(ax = ax, cmap = cmap, ds = ds_new, wave_nm_bin = wave_nm_bin, scattercut = scattercut, bordercut = bordercut,
lintresh = np.max(timelimits), timelimits = timelimits, intensity_range = intensity_range,
title = 'uncorrected select new zero', plot_type = 'lin', use_colorbar = False, log_scale = False)
ax.plot(ax.get_xlim(),[0,0],'black',lw=0.5)
fittingto = np.array(plt.ginput(1)[0])[1]
print(fittingto)
fitcoeff[-1]+=fittingto
ds_new=ds.apply(lambda x:np.interp(x=time+np.polyval(fitcoeff,float(x.name)),xp=time,fp=x),axis=0,raw=False)
plt.close(fig)
fig,ax=plt.subplots()
ax = plot2d(ax = ax, ds = ds_new, cmap = cmap, wave_nm_bin = wave_nm_bin, scattercut = scattercut, bordercut = bordercut,
lintresh = np.max(timelimits), timelimits = timelimits, intensity_range = intensity_range,
title = 'corrected, please select', plot_type = 'lin', use_colorbar = False, log_scale = False)
ax.plot(ax.get_xlim(),[0,0],'black',lw=0.5)
w=(ax.get_xlim()[1]-ax.get_xlim()[0])
ax.add_patch( matplotlib.patches.Rectangle((ax.get_xlim()[0],ax.get_ylim()[0]),w/4,0.2,facecolor='white'))
ax.text(ax.get_xlim()[0],ax.get_ylim()[0]+0.05,'Save',fontsize=30)
ax.add_patch(matplotlib.patches.Rectangle((ax.get_xlim()[0]+w*3/4,ax.get_ylim()[0]),w/4,0.2,facecolor='white'))
ax.text(ax.get_xlim()[0]+w*3/4,ax.get_ylim()[0]+0.05,'Redo',fontsize=30)
satisfied =plt.ginput(1)
if satisfied[0][0] < ax.get_xlim()[0]+w/2:
print('accepted')
plt.close(fig)
break
elif repeat<8:
plt.close(fig)
continue
else:
plt.close(fig)
return False
print(fitcoeff)
if filename is None:
f='chirp.dat'
else:
f=filename.split('.')[0]
f=f+'_chirp' + '.dat'
if path is None:
with open(f, 'w') as opened_file:
opened_file.write(','.join(map(str,np.array(fitcoeff))))
else:
with open(check_folder(path=path,filename=f), 'w') as opened_file:
opened_file.write(','.join(map(str,np.array(fitcoeff))))
return ds_new
[docs]def build_c(times, mod = 'paral', pardf = None, sub_steps = None):
'''
Build concentration matrix after model the parameters are:
resolution is the width of the rise time (at sigma 50% intensity)
This function can also be used to create illustration dynamics.
The parallel decays are created explicit, while the consecutive decays are
created by sampling the populations at the times given in the first vector and
evaluate the progression at a number of substeps defined bu sub_samples (10 by default)
Parameters
-----------
times : np.array
array with the times at which the dataframe should be generated. In general the
experimental times
mod : str, optional
this selects the model that is used to generate the concentrations.
1. 'paral' (Default) or 'exponential' both are equivalent
2. 'consecutive' or 'full_consecutive'
In 2 the 'consecutive' and 'full_consecutive' are different in that for consecutive
the optimization is done using 'exponential' (as it shoudl give the same times)
and then only in the last (final) iteration the 'full consecutive' differential
equation is used. This has significant speed advantages, but can lead to errors particularly
for the very fast times.
sub_step : int, optional
defines how many times the iterative loop (used in consecutive only) is sampling the concentrations
between the times given in "times"
pardf : pd.DataFrame
This dataframe must contain the parameter that are used for creating the dynamics
the parameter must be named with the index.
For the internal functions this must contain these keys:
* 't0' = zero time, mandatory
* 'resolution' = instrument response function, mandatory
* 'background',optional = if this keyword is present a flat constant background is created (=1 over the whole time)
* 'infinite',optional = if this keyword is present a new non decaying component is formed with the last decay time.
* 'explicit_GS',optional = if this keyword is present the pulse function (= ground state) is added explicitly to the data
* 'k0,k1,...' = with increasing integers are taken as decay times. te number of these components is used to determine how many shall be generated.
sub_sample: bool or integer
Default(None) does nothing
This switch turns on a additional sampling of the kinetics, meaning that we add the number of steps between each measured steps for the model formation
usage: sub_sample=10
Examples
---------
'''
if 'sub_steps' in list(pardf.index.values):
sub_steps=pardf['sub_steps']
elif sub_steps is None:
sub_steps=10
choices = {'paral':0,'exponential':0,'consecutive':1,'full_consecutive':1}
model=choices[mod]
param=pardf.loc[pardf.is_rate,'value'].values.astype(float)
t0=float(pardf.loc['t0','value'])
resolution=float(pardf.loc['resolution','value'])
if model == 0:#parallel
c=np.exp(-1*np.tile(times-t0,(len(param),1)).T*param)
c[(times-t0)<0]=1
c*=np.tile(rise(x=times,sigma=resolution,begin=t0),(len(param),1)).T
c=pandas.DataFrame(c,index=times)
c.index.name='time'
if 'background' in list(pardf.index.values):
c['background']=1
if 'infinite' in list(pardf.index.values):
c['infinite']=rise(x=times,sigma=resolution,begin=t0)
return c
if model == 1:#consecutive decays
n_decays=len(param)
g=gauss(times,sigma=resolution/FWHM,mu=t0)
if 'infinite' in list(pardf.index.values):
infinite=True
n_decays+=1
else:
infinite=False
decays=param
c=np.zeros((len(times),n_decays),dtype='float')
if 'explicit_GS' in list(pardf.index.values):
GS=True
gs=np.zeros((len(times),1),dtype='float')
else:
GS=False
for i in range(1,len(times)):
dc=np.zeros((n_decays,1),dtype='float')
dt=(times[i]-times[i-1])/(sub_steps)
c_temp=c[i-1,:]
for j in range(int(sub_steps)):
for l in range(0,n_decays):
if l>0:
if infinite:
if l<(n_decays-1):
dc[l]=decays[l-1]*dt*c_temp[l-1]-decays[l]*dt*c_temp[l]
else:
dc[l]=decays[l-1]*dt*c_temp[l-1]
else:
dc[l]=decays[l-1]*dt*c_temp[l-1]-decays[l]*dt*c_temp[l]
else:
if infinite and n_decays==1:
dc[l]=g[i]*dt
else:
dc[l]=g[i]*dt-decays[l]*dt*c_temp[l]
for b in range(c.shape[1]):
c_temp[b] =np.nanmax([(c_temp[b]+float(dc[b])),0.])
c[i,:] =c_temp
if GS and not infinite:
gs[i]=-c[i,:].sum()
elif GS:
gs[i]=-c[i,:-1].sum()
c=pandas.DataFrame(c,index=times)
c.index.name='time'
if infinite:
labels=list(c.columns.values)
labels[-1]='Non Decaying'
if 'background' in list(pardf.index.values):
c['background']=1
else:
if 'background' in list(pardf.index.values):
c['background']=1
if GS:
c['GS']=gs
return c
[docs]def fill_int(ds,c,final=True,baseunit='ps',return_shapes=False):
'''solving the intensity an equation_way, takes the target dataframe and the concentration frame
prepares the matrixes(c) the tries to solve this equation system using
eps=np.linalg.lstsq(AA,Af,rcond=-1)[0]
if failes it returns a dictionary with 1000 as error (only entry) if successful
it returns a dictionary that contains the
fit_error = (AE**2).sum() with AE beeing the difference of measured and calcuated matrix
Parameters
-----------
ds : DataFrame
DataFrame to be fitted
c: DataFrame
DataFrame oontaining the concentration matrix (the concentrations as with the times as index.
Each different species has a column with the species name as column name
final : bool,optional
if True (Default) the complete solutions will be attached otherwise only the error is attached
baseunit : str,optional
this string is used as unit for the time axis
return_shapes : bool,optional
Default = False, if True, then the concentrations and spectra are added to the re (even if not final)
Returns
------------------
re : dict
the dictionary "re" attached to the object containing all the matrixes and parameter.
if "final" is True:
* "A" Shaped measured Matrix
* "AC" Shaped calculated Matrix
* "AE" Difference between A and AC = linear error
* "DAC" DAS or SAS, labeled after the names given in the function (the columns of c) Care must be taken that this mesured intensity is C * DAS, the product. For exponential model the concentrations are normalized
* "c" The Concentrations (meaning the evolution of the concentrations over time. Care must be taken that this mesured intensity is C * DAS, the product. For exponential model the concentrations are normalized
* "error" is the S2, meaning AE**2.sum().sum()
else:
* "error" is the S2, meaning AE**2.sum()
'''
time=ds.index.values.astype('float')
wl=ds.columns.values.astype('float')
time_label=ds.index.name
energy_label=ds.columns.name
A=ds.values
er=c.values
ert = er.T
AA = np.matmul(ert,er)
Af = np.matmul(ert,A)
try:
eps=np.linalg.lstsq(AA,Af,rcond=-1)[0]
except:
re={'error':1000}
return re
eps[np.isnan(eps)]=0
eps[np.isinf(eps)]=0
AC = np.matmul(er,eps);
AE = A-AC;
fit_error = (AE**2).sum()
if final:
A=pandas.DataFrame(A,index=time,columns=wl)
AC=pandas.DataFrame(AC,index=time,columns=wl)
AE=pandas.DataFrame(AE,index=time,columns=wl)
DAC=pandas.DataFrame(eps.T,index=wl)
A.index.name=time_label
A.columns.name=energy_label
AC.index.name=time_label
AC.columns.name=energy_label
AE.index.name=time_label
AE.columns.name=energy_label
DAC.index.name=energy_label
try:
DAC.columns=c.columns.values
except:
pass
re={'A':A,'AC':AC,'AE':AE,'DAC':DAC,'error':fit_error,'c':c}
elif return_shapes:
re={'DAC':DAC,'error':fit_error,'c':c}
else:
re={'error':fit_error}
return re
[docs]def err_func(paras, ds, mod = 'paral', final = False, log_fit = False, dump_paras = False, write_paras = True,
filename = None, ext_spectra = None, dump_shapes = False, sub_sample=None,pulse_sample=None):
'''function that calculates and returns the error for the global fit. This function is intended for
fitting a single dataset.
Parameters
--------------
ds : DataFrame
This dataframe contains the data to be fitted. This has to be shaped as it is intended to (so all shping parameters
already applied. The dataframe expects the time to be in Index and the wavelength/energy
to be in the columns. The spectra is plotted with a second (energy) axis
paras : lmfit parameter oject
The parameter object that defines what is calculated
mod : str or function, optional
The model selection is depending if it is an internal or external model.
The internal functions are triggered by calling their name
Two main are currently implemented
1. 'paral' (Default) or 'exponential'
2. 'consecutive' or 'full_consecutive'
In 2 the 'consecutive' and 'full_consecutive' are different in that for consecutive
the optimization is done using 'exponential' (as it shoudl give the same times)
and then only in the last (final) iteration the 'full consecutive' differential
equation is used. This has significant speed advantages, but can lead to errors particularly
for the very fast times.
As external model a function is handed to this parameter, this function
must accept the times and an paramater Dataframe and return a DataFrame
with the concentrations (similar to build_c)
for the internal functions:
This datafram must contain the parameter that are used for creadting the dynamics
the parameter must be named with the index.
't0' = zero time, mandatory
'resolution' = instrument response function, mandatory
'background',optional = if this keyword is present a flat constant background is created (=1 over the whole time)
'infinite',optional = if this keyword is present a new non decaying component is formed with the last decay time.
'explicit_GS' = if this keyword is present thenthe ground state (including the bleach) will be added as a explicit component
'k0,k1,...' = with increasing integers are taken as decay times. te number of these components is used to determine how many shall be generated.
final : bool, optional
this switch decides if just the squared error is returned (for False) (Default) or if the full
matrixes are returned, including the r2 are returned.
log_fit : bool, optional
if False (Default) then the parameter are handed to the fitting function as they are, if true
then all times are first converted to log space.
dump_paras : bool, optional
(Default) is False, If True creates two files in the working folder, one with the
currently used parameter created at the end of each optimisation step, and one with
the set of parameter that up to now gave the lowest error. Intented to store
the optimisation results if the fit needs to be interrupted
(if e.g. Ampgo simply needs to long to optimize.) useful option if things are slow
filename : None or str, optional
Only used in conjunction with 'dump_paras'. The program uses this filename to dump the
parameter to disk
ext_spectra : DataFrame, optional
(Default) is None, if given substract this spectra from the DataMatrix using the intensity
given in "C(t)" this function will only work for external models. The name of the spectral column
must be same as the name of the column used. If not the spectrum will be ignored. The spectrum will
be interpolated to the spectral points of the model ds before the substraction.
a number of parameters can be defined to aid this process. These parameter are defined as normal parameters.
"ext_spectra_scale" multiplies all spectra by this value (e.g. -1 to put the steady state absorption spectra in)
"ext_spectra_shift" shifts all spectra by this value to compensate for calibration differences
"ext_spectra_guide" (from version 7.1.0) This is a switch, if this keyword is present, then the spectra are used as guides
and not exclusively. This means the code will assume that these spectra are correct and substract them, then calulate the
difference and return as DAS the provided spectra plus the difference spectra
write_paras : bool, optional
if True(Default) writes the currently varried values to screen
'''
time_label=ds.index.name
energy_label=ds.columns.name
pardf=par_to_pardf(paras)
if log_fit:
pardf.loc[pardf.is_rate,'value']=pardf.loc[pardf.is_rate,'value'].apply(lambda x: 10**x)
if isinstance(mod,type('hello')):#did we use a build in model?
times=ds.index.values.astype('float')
times_ori=times.copy()
if pulse_sample is not None:
t0=float(pardf.loc['t0','value'])
resolution=float(pardf.loc['resolution','value'])
if hasattr(pulse_sample,'__iter__'):pump_region=pulse_sample
else:
pump_region=np.linspace(t0-4*resolution,t0+4*resolution,20)
if pump_region.max()<times_ori.min():
connection_region=np.arange(pump_region[-1],times_ori.min(),resolution/10)
times=np.unique(np.sort(np.hstack((pump_region,connection_region,times_ori))))
else:
times=np.unique(np.sort(np.hstack((pump_region,times_ori))))
if sub_sample is not None:
listen=[times]
for i in range(1,sub_sample,1):
listen.append(times_ori[:-1]+((times_ori[1:]-times_ori[:-1])*i/sub_sample))
times=np.unique(np.hstack(listen))
times.sort()
if final:#for final we really want the model
c=build_c(times=times,mod=mod,pardf=pardf)
elif 'full_consecutive' in mod:# here we force the full consecutive modelling
c=build_c(times=times,mod=mod,pardf=pardf)
else:#here we "bypass" the full consecutive and optimize the rates with the decays
c=build_c(times=times,mod='paral',pardf=pardf)
c=c.loc[times_ori,:]
c.index.name=time_label
if ext_spectra is None:
re=fill_int(ds=ds,c=c, return_shapes = dump_shapes)
else:
if 'ext_spectra_shift' in list(pardf.index.values):
ext_spectra.index=ext_spectra.index.values+pardf.loc['ext_spectra_shift','value']
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
else:
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
if "ext_spectra_scale" in list(pardf.index.values):
ext_spectra=ext_spectra*pardf.loc['ext_spectra_scale','value']
c_temp=c.copy()
for col in ext_spectra.columns:
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
ds=ds-C
if not "ext_spectra_guide" in list(pardf.index.values):
c_temp.drop(col,axis=1,inplace=True)
re=fill_int(ds=ds,c=c_temp, return_shapes = dump_shapes)
if final:
labels=list(re['DAC'].columns.values)
changed=True
if 'background' in list(pardf.index.values):
if 'infinite' in list(pardf.index.values):
labels[-1]='Non Decaying'
labels[-2]='background'
else:
labels[-1]='background'
else:
if 'infinite' in list(pardf.index.values):
labels[-1]='Non Decaying'
else:changed=False
if changed:
re['DAC'].columns=labels
re['c'].columns=labels
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
re['A']=re['A']+C
re['AC']=re['AC']+C
re['r2']=1-re['error']/((re['A']-re['A'].mean().mean())**2).sum().sum()
if dump_paras:
try:
pardf.loc['error','value']=re['error']
except:
pass
try:
pardf.loc['r2','value']=re['r2']
except:
pass
try:
if filename is None:
store_name='minimal_dump_paras.par'
else:
store_name='minimal_dump_paras_%s.par'%filename
min_df=pandas.read_csv(store_name,sep=',',header=None,skiprows=1)
if float(min_df.iloc[-1,1])>float(re['error']):
pardf.to_csv(store_name)
except:
pass
if filename is None:
store_name='dump_paras.par'
else:
store_name='dump_paras_%s.par'%filename
pardf.to_csv(store_name)
return re
else:
if dump_paras:
try:
pardf.loc['error','value']=re['error']
except:
pass
try:
pardf.loc['r2','value']=re['r2']
except:
pass
try:
if filename is None:
store_name='minimal_dump_paras.par'
else:
store_name='minimal_dump_paras_%s.par'%filename
min_df=pandas.read_csv(store_name,sep=',',header=None,skiprows=1)
if float(min_df.iloc[-1,1])>float(re['error']):
pardf.to_csv(store_name)
except:
pass
if filename is None:
store_name='dump_paras.par'
else:
store_name='dump_paras_%s.par'%filename
pardf.to_csv(store_name)
if not mod in ['paral','exponential','consecutive']:
if write_paras:
print('----------------------------------')
print(pardf)
else:
print(re['error'])
if dump_shapes:
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
re['c'].to_csv(path_or_buf=filename + '_c')
re['DAC'].to_csv(path_or_buf=filename + '_DAC')
return re['error']
else:# Nope we used an external model (sorry for the duplication)
times=ds.index.values.astype('float')
times_ori=times.copy()
if pulse_sample is not None:
t0=float(pardf.loc['t0','value'])
resolution=float(pardf.loc['resolution','value'])
if hasattr(pulse_sample,'__iter__'):pump_region=pulse_sample
else:
pump_region=np.linspace(t0-4*resolution,t0+4*resolution,20)
if pump_region.max()<times_ori.min():
connection_region=np.arange(pump_region[-1],times_ori.min(),resolution/10)
times=np.unique(np.sort(np.hstack((pump_region,connection_region,times_ori))))
else:
times=np.unique(np.sort(np.hstack((pump_region,times_ori))))
if sub_sample is not None:
listen=[times]
for i in range(1,sub_sample,1):
listen.append(times_ori[:-1]+((times_ori[1:]-times_ori[:-1])*i/sub_sample))
times=np.unique(np.hstack(listen))
times.sort()
c=mod(times=times,pardf=pardf.loc[:,'value'])
c=c.loc[times_ori,:]
if ext_spectra is None:
re=fill_int(ds=ds,c=c, return_shapes = dump_shapes)
else:
ext_spectra.sort_index(inplace=True)
if 'ext_spectra_shift' in list(pardf.index.values):
ext_spectra.index=ext_spectra.index.values+pardf.loc['ext_spectra_shift','value']
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
else:
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
if "ext_spectra_scale" in list(pardf.index.values):
ext_spectra=ext_spectra*pardf.loc['ext_spectra_scale','value']
c_temp=c.copy()
for col in ext_spectra.columns:
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
ds=ds-C
if not "ext_spectra_guide" in list(pardf.index.values):
c_temp.drop(col,axis=1,inplace=True)
re=fill_int(ds=ds,c=c_temp, return_shapes = dump_shapes)
if final:
if len(re.keys())<3:#
print('error in the calculation')
return re
if ext_spectra is None:
re['DAC'].columns=c.columns.values
re['c'].columns=c.columns.values
else:
re['DAC'].columns=c_temp.columns.values
re['c'].columns=c_temp.columns.values
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
re['A']=re['A']+C
re['AC']=re['AC']+C
re['r2']=1-re['error']/((re['A']-re['A'].mean().mean())**2).sum().sum()
if dump_paras:
try:
pardf.loc['error','value']=re['error']
except:
pass
try:
min_df=pandas.read_csv('minimal_dump_paras.par',sep=',',header=None,skiprows=1)
if float(min_df.iloc[-1,1])>float(re['error']):
pardf.to_csv('minimal_dump_paras.par')
except:
pass
pardf.to_csv('dump_paras.par')
return re
else:
if dump_paras:
try:
pardf.loc['error','value']=re['error']
except:
pass
try:
min_df=pandas.read_csv('minimal_dump_paras.par',sep=',',header=None,skiprows=1)
if float(min_df.iloc[-1,1])>float(re['error']):
pardf.to_csv('minimal_dump_paras.par')
except:
pass
pardf.to_csv('dump_paras.par')
if write_paras:
print('----------------------------------')
print(pardf)
else:
print(re['error'])
if dump_shapes:
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
re['c'].to_csv(path_or_buf=filename + '_c')
re['DAC'].to_csv(path_or_buf=filename + '_DAC')
return re['error']
[docs]def err_func_multi(paras, mod = 'paral', final = False, log_fit = False, multi_project = None,
unique_parameter = None, weights = None, dump_paras = False, filename = None,
ext_spectra = None, dump_shapes = False, same_DAS = False,
write_paras = True,sub_sample=None,pulse_sample=None):
'''function that calculates and returns the error for the global fit. This function is intended for
fitting of multiple datasets
Parameters
--------------
paras : lmfit parameter oject
The parameter object that defines what is calculated
mod : str or function, optional
The model selection is depending if it is an internal or external model.
The internal functions are triggered by calling their name
Two main are currently implemented
1. 'paral' (Default) or 'exponential'
2. 'consecutive' or 'full_consecutive'
In 2 the 'consecutive' and 'full_consecutive' are different in that for consecutive
the optimization is done using 'exponential' (as it shoudl give the same times)
and then only in the last (final) iteration the 'full consecutive' differential
equation is used. This has significant speed advantages, but can lead to errors particularly
for the very fast times.
for the internal functions:
This datafram must contain the parameter that are used for creadting the dynamics
the parameter must be named with the index.
't0' = zero time, mandatory
'resolution' = instrument response function, mandatory
'background',optional = if this keyword is present a flat constant background is created (=1 over the whole time)
'infinite',optional = if this keyword is present a new non decaying component is formed with the last decay time.
'explicit_GS' = if this keyword is present thenthe ground state (including the bleach) will be added as a explicit component
'k0,k1,...' = with increasing integers are taken as decay times. te number of these components is used to determine how many shall be generated.
As external model a function is handed to this parameter, this function
must accept the times and an paramater Dataframe and return a DataFrame
with the concentrations (similar to build_c)
final : bool, optional
this switch decides if just the squared error is returned (for False) (Default) or if the full
matrixes are returned, including the r2 are returned.
log_fit : bool, optional
if False (Default) then the parameter are handed to the fitting function as they are, if true
then all times are first converted to log space.
dump_paras : bool, optional
(Default) is False, If True creates two files in the working folder, one with the
currently used parameter created at the end of each optimisation step, and one with
the set of parameter that up to now gave the lowest error. Intented to store
the optimisation results if the fit needs to be interrupted
(if e.g. Ampgo simply needs to long to optimize.) useful option if things are slow
filename : None or str, optional
Only used in conjunction with 'dump_paras'. The program uses this filename to dump the
parameter to disk
multi_project : None or list (of TA projects), optional
This switch is triggering the simultaneous optimisation of multiple datasets.
multi_project is as (Default) None. it expects an iterable (typically list) with other
TA projects (like ta) that are then optimised with the same parameter.
This means that all projects get the same parameter object for each iteration
of the fit and return their individual error, which is summed linearly.
The "weights" option allows to give each multi_project a specific weight (number)
that is multiplied to the error. If the weight object has the same number of items
as the multi_project it is assumed that the triggering object (the embedded project)
has the weight of 1, otherwise the first weight is for the embedded project.
The option 'unique_parameter' takes (a list) of parameter that are not
to be shared between the projects (and that are not optimized either)
The intended use of this is to give e.g. the pump power for multiple experiments to
study non linear behaviour. Returned will be only the parameter set for the optimium
combination of all parameter. Internally, we iterate through the projects and calculate
for each project the error for each iteration. Important to note is that currently this
means that each DAS/SAS is calculated independently! For performing the same calculation
with a single DAS, the Matrixes need to be concatenated before the run and an external
function used to create a combined model. As this is very difficult to implement reliably
For general use (think e.g. different pump wavelength) this has to be done manually.
unique_parameter : None or str or list (of strings), optional
only used in conjunction with 'multi_project', it takes (a list) of parameter that
are not to be shared between the projects (and that are not optimized either)
The intended use of this is to give e.g. the pump power for multiple experiments
to study non linear behaviour. (Default) None
same_DAS : bool,optional
changes the fit behavior and uses the same DAS for the optimization. This means that the ds are stacked before the fill int rounds
weights : list of floats, optional
only used in conjunction with 'multi_project'. The "weights" option allows to
give each multi\_project a specific weight (number) that is multiplied to the error.
If the weight object has the same number of items as the 'multi_project' it is assumed
that ta (the embedded project) has the weight of 1, otherwise the first weight is for the
embedded object
ext_spectra : DataFrame, optional
(Default) is None, if given substract this spectra from the DataMatrix using the intensity
given in "C(t)" this function will only work for external models. The name of the spectral column
must be same as the name of the column used. If not the spectrum will be ignored. The spectrum will
be interpolated to the spectral points of the model ds before the substraction.
a number of parameters can be defined to aid this process. These parameter are defined as normal parameters.
"ext_spectra_scale" multiplies all spectra by this value (e.g. -1 to put the steady state absorption spectra in)
"ext_spectra_shift" shifts all spectra by this value to compensate for calibration differences
"ext_spectra_guide" (from version 7.1.0) This is a switch, if this keyword is present, then the spectra are
used as guides and not exclusively. This means the code will assume that these spectra are correct and
substract them, then calulate the difference and return as DAS the provided spectra plus the difference spectra
write_paras : bool, optional
if True(Default) writes the currently varried values to screen
'''
pardf_changing=par_to_pardf(paras)
error_listen=[]
r2_listen=[]
slice_setting_object=multi_project[0].Copy()
####### new same DAS, I'm lazy and will doublicate te loop. ###########
if same_DAS:
c_stack=[]
ds_stack=[]
par_stack=[]
height_stack=[]
for i,ta in enumerate(multi_project):
ds = sub_ds(ds = ta.ds, scattercut = slice_setting_object.scattercut, bordercut = slice_setting_object.bordercut,
timelimits = slice_setting_object.timelimits, wave_nm_bin = slice_setting_object.wave_nm_bin,
time_bin = slice_setting_object.time_bin, ignore_time_region = slice_setting_object.ignore_time_region)
pardf=pardf_changing.copy()
try:#let's see if the project has an parameter object
pardf_ori=par_to_pardf(ta.par)
except:
pardf_ori=ta.pardf.copy()
if unique_parameter is not None:
for key in unique_parameter:
pardf.loc[key,'value']=pardf_ori.loc[key,'value']
par_stack.append(pardf)
if log_fit:
pardf.loc[pardf.is_rate,'value']=pardf.loc[pardf.is_rate,'value'].apply(lambda x: 10**x)
times=ds.index.values.astype('float')
times_ori=times.copy()
if pulse_sample is not None:
t0=float(pardf.loc['t0','value'])
resolution=float(pardf.loc['resolution','value'])
if hasattr(pulse_sample,'__iter__'):pump_region=pulse_sample
else:
pump_region=np.linspace(t0-4*resolution,t0+4*resolution,20)
if pump_region.max()<times_ori.min():
connection_region=np.arange(pump_region[-1],times_ori.min(),resolution/10)
times=np.unique(np.sort(np.hstack((pump_region,connection_region,times_ori))))
else:
times=np.unique(np.sort(np.hstack((pump_region,times_ori))))
if sub_sample is not None:
listen=[times]
for i in range(1,sub_sample,1):
listen.append(times_ori[:-1]+((times_ori[1:]-times_ori[:-1])*i/sub_sample))
times=np.unique(np.hstack(listen))
times.sort()
if isinstance(mod,type('hello')):#did we use a build in model?
c=build_c(times=ds.index.values.astype('float'),mod=mod,pardf=pardf)
else:
c=mod(times=ds.index.values.astype('float'),pardf=pardf.loc[:,'value'])
c=c.loc[times_ori,:]
if ext_spectra is None:
c_temp=c.copy()
else:
if 'ext_spectra_shift' in list(pardf.index.values):
ext_spectra.index=ext_spectra.index.values+pardf.loc['ext_spectra_shift','value']
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
else:
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
if "ext_spectra_scale" in list(pardf.index.values):
ext_spectra=ext_spectra*pardf.loc['ext_spectra_scale','value']
c_temp=c.copy()
for col in ext_spectra.columns:
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
ds=ds-C
if not "ext_spectra_guide" in list(pardf.index.values):
c_temp.drop(col,axis=1,inplace=True)
if not weights is None:
if len(weights)==len(multi_project)-1:
weights=list(weights)
weights.insert(0,1)
elif len(weights)!=len(multi_project):
Ex = ValueError()
Ex.strerror='The number of entries i the list must either be the number of all elements (including \"TA\" or the number of elements in other. In this case the element ta gets the weight=1'
raise Ex
ds_stack.append(ds*weights[i])
else:
ds_stack.append(ds)
c_stack.append(c_temp)
height_stack.append(len(c_temp.index.values))
A_con=pandas.concat(ds_stack)
c_con=pandas.concat(c_stack)
re=fill_int(ds=A_con,c=c_con, return_shapes = dump_shapes, final =final)
if final:
re['c']=c
if dump_paras:
try:
pardf.loc['error','value']=re['error']
except:
pass
if final:
try:
pardf.loc['r2','value']=1-re['error']/((re['A']-re['A'].mean().mean())**2).sum().sum()
except:
pass
try:
if filename is None:
store_name='minimal_dump_paras.par'
else:
store_name='minimal_dump_paras_%s.par'%filename
min_df=pandas.read_csv(store_name,sep=',',header=None,skiprows=1)
if float(min_df.iloc[-1,1])>float(combined_error):
pardf.to_csv(store_name)
except:
pass
if filename is None:
store_name='dump_paras.par'
else:
store_name='dump_paras_%s.par'%filename
try:
pardf.to_csv(store_name)
except:
print('Saving of %s failed'%store_name)
if final:
if isinstance(mod,type('hello')):#did we use a build in model?
labels=list(re['DAC'].columns.values)
changed=True
if 'background' in list(pardf.index.values):
if 'infinite' in list(pardf.index.values):
labels[-1]='Non Decaying'
labels[-2]='background'
else:
labels[-1]='background'
else:
if 'infinite' in list(pardf.index.values):
labels[-1]='Non Decaying'
else:changed=False
if changed:
re['DAC'].columns=labels
re['c'].columns=labels
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
re['A']=re['A']+C
re['AC']=re['AC']+C
else:
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
re['A']=re['A']+C
re['AC']=re['AC']+C
else:
re['DAC'].columns=c.columns.values
re['c'].columns=c.columns.values
return_listen=[]
for i,ta in enumerate(multi_project):
re_local={}
if i==0:
lower=0
else:
lower=np.array(height_stack)[:i].sum()
re_local['A']=re['A'].copy().iloc[lower:lower+height_stack[i],:]
re_local['AC']=re['AC'].copy().iloc[lower:lower+height_stack[i],:]
re_local['AE']=re['AE'].copy().iloc[lower:lower+height_stack[i],:]
re_local['c']=re['c'].copy().iloc[lower:lower+height_stack[i],:]
re_local['error_total']=re['error']
re_local['error']=(re['AE']**2).sum().sum()
re_local['DAC']=re['DAC'].copy()
re_local['r2']=1-re_local['error']/((re_local['A']-re_local['A'].mean().mean())**2).sum().sum()
re_local['r2_total']=1-re['error']/((re['A']-re['A'].mean().mean())**2).sum().sum()
re_local['pardf']=par_stack[i]
try:
re_local['filename']=filename
except:
pass
return_listen.append(re_local)
if not mod in ['paral','exponential','consecutive']:
if write_paras:
print('----------------------------------')
print(pardf)
else:
print(re['error'])
if final:
return return_listen
else:
return re['error']
################### not same DAS####################
else:
for i,ta in enumerate(multi_project):
ds = sub_ds(ds = ta.ds, scattercut = slice_setting_object.scattercut, bordercut = slice_setting_object.bordercut,
timelimits = slice_setting_object.timelimits, wave_nm_bin = slice_setting_object.wave_nm_bin,
time_bin = slice_setting_object.time_bin, ignore_time_region = slice_setting_object.ignore_time_region)
pardf=pardf_changing.copy()
try:#let's see if the project has an parameter object
pardf_ori=par_to_pardf(ta.par)
except:
pardf_ori=pardf
if unique_parameter is not None:
for key in unique_parameter:
pardf.loc[key,'value']=pardf_ori.loc[key,'value']
if log_fit:
pardf.loc[pardf.is_rate,'value']=pardf.loc[pardf.is_rate,'value'].apply(lambda x: 10**x)
if isinstance(mod,type('hello')):#did we use a build in model?
times=ds.index.values.astype('float')
times_ori=times.copy()
if pulse_sample is not None:
t0=float(pardf.loc['t0','value'])
resolution=float(pardf.loc['resolution','value'])
if hasattr(pulse_sample,'__iter__'):pump_region=pulse_sample
else:
pump_region=np.linspace(t0-4*resolution,t0+4*resolution,20)
if pump_region.max()<times_ori.min():
connection_region=np.arange(pump_region[-1],times_ori.min(),resolution/10)
times=np.unique(np.sort(np.hstack((pump_region,connection_region,times_ori))))
else:
times=np.unique(np.sort(np.hstack((pump_region,times_ori))))
if sub_sample is not None:
listen=[times]
for i in range(1,sub_sample,1):
listen.append(times_ori[:-1]+((times_ori[1:]-times_ori[:-1])*i/sub_sample))
times=np.unique(np.hstack(listen))
times.sort()
c=build_c(times=ds.index.values.astype('float'),mod=mod,pardf=pardf)
c=c.loc[times_ori,:]
if ext_spectra is None:
re=fill_int(ds=ds,c=c, return_shapes = dump_shapes)
else:
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
c_temp=c.copy()
for col in ext_spectra.columns:
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
ds=ds-C
c_temp.drop(col,axis=1,inplace=True)
re=fill_int(ds=ds,c=c_temp, return_shapes = dump_shapes)
if final:
if i==0:
labels=list(re['DAC'].columns.values)
changed=True
if 'background' in list(pardf.index.values):
if 'infinite' in list(pardf.index.values):
labels[-1]='Non Decaying'
labels[-2]='background'
else:
labels[-1]='background'
else:
if 'infinite' in list(pardf.index.values):
labels[-1]='Non Decaying'
else:changed=False
if changed:
re['DAC'].columns=labels
re['c'].columns=labels
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
re['A']=re['A']+C
re['AC']=re['AC']+C
re_final=re.copy()
error_listen.append(re['error'])
r2_listen.append(1-re['error']/((re['A']-re['A'].mean().mean())**2).sum().sum())
else:
if dump_shapes:
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
re['c'].to_csv(path_or_buf=ta.filename + '_c')
re['DAC'].to_csv(path_or_buf=ta.filename + '_DAC')
error_listen.append(re['error'])
else:
times=ds.index.values.astype('float')
times_ori=times.copy()
if pulse_sample is not None:
t0=float(pardf.loc['t0','value'])
resolution=float(pardf.loc['resolution','value'])
if hasattr(pulse_sample,'__iter__'):pump_region=pulse_sample
else:
pump_region=np.linspace(t0-4*resolution,t0+4*resolution,20)
if pump_region.max()<times_ori.min():
connection_region=np.arange(pump_region[-1],times_ori.min(),resolution/10)
times=np.unique(np.sort(np.hstack((pump_region,connection_region,times_ori))))
else:
times=np.unique(np.sort(np.hstack((pump_region,times_ori))))
if sub_sample is not None:
listen=[times]
for i in range(1,sub_sample,1):
listen.append(times_ori[:-1]+((times_ori[1:]-times_ori[:-1])*i/sub_sample))
times=np.unique(np.hstack(listen))
times.sort()
c=mod(times=ds.index.values.astype('float'),pardf=pardf.loc[:,'value'])
c=c.loc[times_ori,:]
if ext_spectra is None:
re=fill_int(ds=ds,c=c, return_shapes = dump_shapes)
else:
ext_spectra=rebin(ext_spectra,ds.columns.values.astype(float))
c_temp=c.copy()
for col in ext_spectra.columns:
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
ds=ds-C
if not "ext_spectra_guide" in list(pardf.index.values):
c_temp.drop(col,axis=1,inplace=True)
re=fill_int(ds=ds,c=c_temp, return_shapes = dump_shapes)
if final:
if i==0:
re['DAC'].columns=c.columns.values
re['c'].columns=c.columns.values
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
A,B=np.meshgrid(c.loc[:,col].values,ext_spectra.loc[:,col].values)
C=pandas.DataFrame((A*B).T,index=c.index,columns=ext_spectra.index.values)
re['A']=re['A']+C
re['AC']=re['AC']+C
re_final=re.copy()
error_listen.append(re['error'])
r2_listen.append(1-re['error']/((re['A']-re['A'].mean().mean())**2).sum().sum())
else:
if dump_shapes:
if not ext_spectra is None:
for col in ext_spectra.columns:
if "ext_spectra_guide" in list(pardf.index.values):
re['DAC'][col]=re['DAC'][col]+ext_spectra.loc[:,col].values
else:
re['DAC'][col]=ext_spectra.loc[:,col].values
re['c'][col]=c.loc[:,col].values
re['c'].to_csv(path_or_buf=filename + '_c')
re['DAC'].to_csv(path_or_buf=filename + '_DAC')
error_listen.append(re['error'])
if not weights is None:
if len(weights)==len(error_listen)-1:
weights=list(weights)
weights.insert(0,1)
elif len(weights)!=len(error_listen):
Ex = ValueError()
Ex.strerror='The number of entries i the list must either be the number of all elements (including \"TA\" or the number of elements in other. In this case the element ta gets the weight=1'
raise Ex
combined_error=np.sqrt(((np.array(error_listen)*np.array(weights))**2).mean())
if final:
combined_r2=np.sqrt(((np.array(r2_listen)*np.array(weights))**2).mean())
else:
combined_error=np.sqrt((np.array(error_listen)**2).mean())
if final:
combined_r2=np.sqrt(((np.array(r2_listen))**2).mean())
if final:
re_final['error']=combined_error
re_final['r2']=combined_r2
if dump_paras:
try:
pardf.loc['error','value']=combined_error
except:
pass
try:
pardf.loc['r2','value']=combined_r2
except:
pass
try:
if filename is None:
store_name='minimal_dump_paras.par'
else:
store_name='minimal_dump_paras_%s.par'%filename
min_df=pandas.read_csv(store_name,sep=',',header=None,skiprows=1)
if float(min_df.iloc[-1,1])>float(combined_error):
pardf.to_csv(store_name)
except:
pass
if filename is None:
store_name='dump_paras.par'
else:
store_name='dump_paras_%s.par'%filename
try:
pardf.to_csv(store_name)
except:
print('Saving of %s failed'%store_name)
if not mod in ['paral','exponential','consecutive']:
print(combined_error)
if final:
return re_final
else:
return combined_error
def par_to_pardf(par):
'''function to convert a parameters object into a pretty DataFrame, it expects par to be a lmfit parameters object and loops through the keys'''
out_dicten={}
for key in par.keys():
out_dicten[key]={'value':par[key].value}
if key[0] == 'k':#its a time parameter
out_dicten[key]['is_rate']=True
else:
out_dicten[key]['is_rate']=False
out_dicten[key]['min']=par[key].min
out_dicten[key]['max']=par[key].max
out_dicten[key]['vary']=par[key].vary
out_dicten[key]['expr']=par[key].expr
return pandas.DataFrame(out_dicten).T
def pardf_to_par(par_df):
'''converts a dataframe to lmfit object
set(value=None, vary=None, min=None, max=None, expr=None, brute_step=None)'''
par=lmfit.Parameters()
for key in par_df.index.values:
par.add(key, value=par_df.loc[key,'value'], vary=par_df.loc[key,'vary'], min=par_df.loc[key,'min'], max=par_df.loc[key,'max'], expr=par_df.loc[key,'expr'])
return par
def pardf_to_timedf(pardf):
'''inverts all the rates to times in a dataframe'''
timedf=pardf.copy()
if 'upper_limit' in pardf.keys():
for key in ['init_value','value','min','max','lower_limit','upper_limit']:
for row in pardf.index.values:
if timedf.loc[row,'is_rate']:
if key == 'min':key_in='max'
elif key == 'max':key_in='min'
elif key == 'lower_limit':key_in='upper_limit'
elif key == 'upper_limit':key_in='lower_limit'
else:key_in=key
try:
if pardf.loc[row,key] !=0:
timedf.loc[row,key_in]=1/pardf.loc[row,key]
else:
timedf.loc[row,key_in]='inf'
except:
if key == 'init_value':pass#we don't save the init values, so we get an error when converting the saved file
elif pardf.loc[row,key] is None:continue
else:print('conversion of this key failed: %s %s'%(row,key))
else:
for key in ['init_value','value','min','max']:
if key == 'min':key_in='max'
elif key == 'max':key_in='min'
else:key_in=key
try:
timedf.loc[pardf.is_rate,key_in]=pardf.loc[pardf.is_rate,key].apply(lambda x: 1/x if x!=0 else 'inf')
except:
if key == 'init_value':pass#we don't save the init values, so we get an error when converting the saved file
else:print('conversion of this key failed:' + key)
return timedf
[docs]class TA(): # object wrapper for the whole
[docs] def __init__(self, filename, path = None, sep = "\t", decimal = '.', index_is_energy = False, transpose = False,
sort_indexes = False, divide_times_by = None, shift_times_by = None, external_time = None, external_wave = None,
use_same_name = True, data_type = None , units = None, baseunit = None, ds = None, conversion_function = None):
'''Function that opens and imports data into an TA object
it is designed to open combined files that contain both the wavelength and the time. (e.g. SIA files as recorded by Pascher instruments software) or hdf5 projects saved by this software
There are however a lot of additional options to open other ascii type files and adapt their format internally
Attention times with Nan will be completely removed during the import
Parameters
----------
filename : str
* expects a filename in string form for opening a single file.
* alternatively 'gui' can be set as filename, then a TKinter gui is opened for select.
* alternatively 'recent' can given as key word. in this case it tries to find a text file named "recent.dat" that should contain the path to the last file opened with the GUI. this file is then opened. if this file is not found the GUI is opened instead
path : str or path object (optional)
if path is a string without the operation system dependent separator, it is treated as a relative path,
e.g. data will look from the working directory in the sub director data. Otherwise this has to be a
full path in either strong or path object form.
sep : str (optional)
is the separator between different numbers, typical are tap (Backslash t) (Default) ,one or
multiple white spaces 'backslash s+' or comma ','.
decimal : str (optional)
sets the ascii symbol that is used for the decimal sign. In most countries this is '.'(Default)
but it can be ',' in countries like Sweden or Germany
index_is_energy : bool (optional)
switches if the wavelength is given in nm (Default) or in eV (if True), currently everything
is handled as wavelength in nm internally
data_type: str (optional)
data_type is the string that represents the intensity measurements. Usually this contains if absolute
of differential data. This is used for the color intensity in the 2d plots and the y-axis for the 1d plots
units: str (optional)
this is used to identify the units on the energy axis and to label the slices, recognized is 'nm', 'eV' and 'keV'
but if another unit like 'cm^-1' is used it will state energy in 'cm^-1'. Pleas observe that if you use the index_is_energy
switch the program tries to convert this energy into wavelength.
baseunit: str (optional)
this is used to identify the units on the developing/time axis. This is name that is attached to the index of the dataframe.
setting this during import is equivalent to ta.baseunit
transpose : bool (optional)
if this switch is False (Default) the wavelength are the columns and the rows the times.
sort_indexes : bool (optional)
For False (Default) I assume that the times and energies are already in a rising order.
with this switch, both are sorted again.
divide_times_by : None or float (optional)
here a number can be given that scales the time by an arbitary factor. This is actually dividing
the times by this value. Alternatively there is the variable self.baseunit. The latter only affects
what is written on the axis, while this value is actually used to scale the times. None (Default)
ignores this
shift_times_by : None, float (optional)
This a value by which the time axis is shifted during import. This is a useful option of e.g.
the recording software does not compensate for t0 and the data is always shifted.
None (Default) ignores this setting
data_type : str, None
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
external_time : None or str (optional)
Here a filename extension (string) can be given that contains the time vector.
The file is assumed to be at the same path as the data and to contain a single
type of separated data without header.
If use_same_name = True (default)
It assumes that this is the ending for the file. The filename itself is taken from the filename.
e.g. if samp1.txt is the filename and external_time='.tid' the program searches
samp1.tid for the times. The transpose setting is applied and sets where the times are
to be inserted (row or column indexes)
If use_same_name = False this should be the file containing the vector for the time (in the same format as the main file)
external_wave : None or str (optional)
Here a filename extension (string) can be given that contains the wavelength vector.
If use_same_name = True (default)
The file is assumed to be at the same path as the data and to contain a single type
of separated data without header. This is the ending for the file. The filename itself
is taken from the filename. e.g. if samp1.txt is the filename and external_wave='.wav'
then the program searches samp1.wav for the wavelength. The transpose setting is applied
and sets where the wavelength are to be inserted (columns or row indexes)
If use_same_name = False
this should be a full filename that contains the vector
use_same_name : bool, optional
this switches if the external filename included the loaded filename or is a separate file True(default)
ds: pandas.DataFrame (optional)
feed in an external dataframe instead of opening a file
conversion_function: function(optional)
function that receives should have the shape:
return pandas Dataframe with time/frames in rows and wavelength/energy in columns,
The function is tested to accept (in that order) a
my_function(filename, external_time,external_wave),
my_function(filename, external_time),
my_function(filename,external_wave),
my_function(filename) and
return: the dataframe ds with the time_axis as rows and spectral axis as columns
if the ds.index.name ia not empty the "time axis" is in to that name the spectral axis is in ds.columns.name
the return is investigated if it is one, two, or three things.
if two are returned then the second must be the name of what the intensity axis is. This value will then be set to data_type
if three are returned the third is the baseunit (for the time axis) this allows to use the automatic naming in ps or nanosecond
If the values units, data_type or baseunit are (manually) set in the import function the corresponding entries in
datafram will be overwritten
shift_times_by and divide_times_by will be applied if not None (useful to adjust for offset before chirp correction)
Returns
-------
A TA object with all parameter initialized
Examples
--------------
Typical useage:
>>> import plot_func as pf #import the module and give it a shorter name
>>> ta=pf.TA('gui') #use a GUI to open
>>> ta=pf.TA('sample_1.SIA') #use a filename in the same folder
>>> ta=pf.TA('sample_1.hdf5',path='Data') #use a filename in the folder 'Data'
Opening a list of files with external time vector (of the same name) so it looks for a data
file "fite1.txt" and a file with the time information "file1.tid"
>>>ta=pf.TA('file1.txt', external_time = 'tid')
'''
self.filename=filename
self.path=check_folder(path=path,current_path=os.getcwd())
if ds is not None:
if filename is None:
filename = 'external'
self.filename=filename
else:
filename = 'external'
if filename == 'gui':
root_window = tkinter.Tk()
root_window.withdraw()
root_window.attributes('-topmost',True)
root_window.after(1000, lambda: root_window.focus_force())
complete_path = filedialog.askopenfilename(initialdir=os.getcwd())
listen=os.path.split(complete_path)
path=os.path.normpath(listen[0])
self.path=path
filename=listen[1]
self.filename=filename
with open('recent.dat','w') as f:
f.write(complete_path)
elif filename == 'recent':
try:
with open('recent.dat','r') as f:
complete_path = f.readline()
listen=os.path.split(complete_path)
path=os.path.normpath(listen[0])
self.path=path
filename=listen[1]
self.filename=filename
except:
root_window = tkinter.Tk()
root_window.withdraw()
root_window.attributes('-topmost',True)
root_window.after(1000, lambda: root_window.focus_force())
complete_path = filedialog.askopenfilename(initialdir=os.getcwd())
listen=os.path.split(complete_path)
path=os.path.normpath(listen[0])
self.path=path
filename=listen[1]
self.filename=filename
with open('recent.dat','w') as f:
f.write(complete_path)
if filename == 'external':#use a provided dataframe (ds) instead
if data_type is not None:
self.data_type = data_type
if units is not None:
self.units = units
try:
if len(ds.columns.name)==0:
ds.columns.name= units
except:
pass
else:
try:
if len(ds.columns.name)!=0:
self.units = ds.columns.name
except:
pass
if baseunit is not None:
self.baseunit = baseunit
try:
if len(ds.index.name)==0:
if (baseunit == 'ps') or (baseunit == 'ns'):
ds.index.name='Time in %s'%baseunit
else:
ds.index.name= baseunit
except:
pass
else:
try:
if len(ds.index.name)!=0:
self.baseunit = ds.index.name
except:
pass
self.ds_ori=ds
self.ds=ds
self.__make_standard_parameter()
elif ('hdf5' in filename) and (conversion_function is None):#we load a conversion function to deal with the file:#we read in data from previous run
self.__read_project(saved_project=check_folder(path=self.path,filename=self.filename))
self.__make_standard_parameter()
self.Cor_Chirp(fitcoeff=self.fitcoeff)
else:#we read in raw data from sia File
if conversion_function is not None:
filename=check_folder(path=self.path,filename=filename)
try:
ret=conversion_function(filename = filename, external_time = external_time, external_wave = external_wave)
except:
try:
ret=conversion_function(filename = filename, external_time = external_time)
except:
try:
ret=conversion_function(filename = filename, external_wave = external_wave)
except:
try:
ret=conversion_function(filename = filename)
except Exception as e:
print(e)
return False
if isinstance(ret,pandas.DataFrame):
##import is what we wanted
ds=ret
elif isinstance(ret,pandas.Series):
ds=ret.as_frame()
else:
if len(ret) == 2:
if data_type is None:
ds,data_type=ret
else:
ds,_=ret
elif len(ret) == 3:
if data_type is None:
ds,data_type,baseunit=ret
else:
ds,_,baseunit=ret
else:
print('sorry the return format of the conversion_function was not understood')
print('return: the dataframe ds with the time_axis as rows and spectral axis as columns\n')
print('if the ds.index.name ia not empty the "time axis" is in to that name the spectral axis is in ds.columns.name\n')
print('the return is investigated if it is one, two, or three things.\n ')
print('if two are returned then the second must be the name of what the intensity axis is. This value will then be set to data_type\n')
print('if three are returned the third is the baseunit (for the time axis) this allows to use the automatic naming in ps or ns ' )
return False
## see if we have the name a data types in the data
if data_type is not None:
self.data_type = data_type
if units is not None:
self.units = units
try:
if len(ds.columns.name)==0:
ds.columns.name= units
except:
pass
else:
try:
if len(ds.columns.name)!=0:
self.units = ds.columns.name
except:
pass
if baseunit is not None:
self.baseunit = baseunit
try:
if len(ds.index.name)==0:
if (baseunit == 'ps') or (baseunit == 'ns'):
ds.index.name='Time in %s'%baseunit
else:
ds.index.name= baseunit
except:
pass
else:
try:
if len(ds.index.name)!=0:
self.baseunit = ds.index.name
except:
pass
if shift_times_by is not None:
ds.index=ds.index.values+shift_times_by
if divide_times_by is not None:
ds.index=ds.index.values/divide_times_by
self.ds_ori=ds
self.ds=ds
else:
self.__read_ascii_data(sep = sep, decimal = decimal, index_is_energy = index_is_energy,
transpose = transpose, sort_indexes = sort_indexes,
divide_times_by = divide_times_by, shift_times_by = shift_times_by,
external_time = external_time, external_wave = external_wave,
use_same_name = use_same_name, data_type = data_type, units = units,
baseunit = baseunit)
self.__make_standard_parameter()
if len(self.ds.columns.values)<2:
self.rel_wave=[self.ds.columns.values[0]]
def __call__(self):
print('''
Hello, this is a transient absorption project,
this project contains all the parameter and
the main functions you can use to analyze data.
-----------------------------------------------
The main functions are:
Plotting: ta.Plot_RAW(), ta.Plot_Interactive(), ta.Plot_fit_output()
Comparing: ta.Compare_at_time(), ta.Compare_at_wave(), ta.Compare_DAC()
Saving: ta.Save_Powerpoint(), ta.Save_project(), ta.Save_data(), ta.Save_Plots()
Fitting: ta.Fit_Global()
Shaping: ta.Background(), ta.Cor_Chirp(), ta.Man_Chirp()
Parameter: call "ta.__dict__.keys()" to see all implemented shaping parameter
-------------------------------------------------------------------------------
you can get help and inspiration how to use each function by typing the function
name followed by a question mark like "ta.Fit_Global?"
or by going to the documentation webpage:
https://kimopack.readthedocs.io/en/latest
--------------------------------------------------------------------------------
In general I recommend to start by using one of the workflow notebooks that you can
download by calling pf.download_notebooks() or by going to
https://github.com/erdzeichen/KiMoPack/tree/main/Workflow_tools''')
def __read_ascii_data(self, sep = "\t", decimal = '.', index_is_energy = False, transpose = False,
sort_indexes = False, divide_times_by = None, shift_times_by = None,
external_time = None, external_wave = None, use_same_name = True, correct_ascii_errors = True,
data_type = None, units = None, baseunit = None):
'''Fancy function that handles the import of pure ascii files.
Parameters
----------
sep : str (optional)
is the separator between different numbers, typical are tap (Backslash t) (Default) ,one or
multiple white spaces 'backslash s+' or comma ','.
decimal : str (optional)
sets the ascii symbol that is used for the decimal sign. In most countries this is '.'(Default)
but it can be ',' in countries like Sweden or Germany
index_is_energy : bool (optional)
switches if the wavelength is given in nm (Default) or in eV (if True), currently everything
is handled as wavelength in nm internally
data_type: str (optional)
data_type is the string that represents the intensity measurements. Usually this contains if absolute
of differential data. This is used for the color intensity in the 2d plots and the y-axis for the 1d plots
units: str (optional)
this is used to identify the units on the energy axis and to label the slices, recognized is 'nm', 'eV' and 'keV'
but if another unit like 'cm^-1' is used it will state energy in 'cm^-1'. Pleas observe that if you use the index_is_energy
switch the program tries to convert this energy into wavelength.
baseunit: str (optional)
this is used to identify the units on the developing/time axis. This is name that is attached to the index of the dataframe.
setting this during import is equivalent to ta.baseunit
transpose : bool (optional)
if this switch is False (Default) the wavelength are the columns and the rows the times.
sort_indexes : bool (optional)
For False (Default) I assume that the times and energies are already in a rising order.
with this switch, both are sorted again.
divide_times_by : None or float (optional)
here a number can be given that scales the time by an arbitary factor. This is actually dividing
the times by this value. Alternatively there is the variable self.baseunit. The latter only affects
what is written on the axis, while this value is actually used to scale the times. None (Default)
ignores this
shift_times_by : None, float (optional)
This a value by which the time axis is shifted during import. This is a useful option of e.g.
the recording software does not compensate for t0 and the data is always shifted.
None (Default) ignores this setting
external_time : None or str (optional)
Here a filename extension (string) can be given that contains the time vector.
The file is assumed to be at the same path as the data and to contain a single
type of separated data without header.
If use_same_name = True (default)
It assumes that this is the ending for the file. The filename itself is taken from the filename.
e.g. if samp1.txt is the filename and external_time='.tid' the program searches
samp1.tid for the times. The transpose setting is applied and sets where the times are
to be inserted (row or column indexes)
If use_same_name = False this should be the file containing the vector for the time (in the same format as the main file)
external_wave : None or str (optional)
Here a filename extension (string) can be given that contains the wavelength vector.
If use_same_name = True (default)
The file is assumed to be at the same path as the data and to contain a single type
of separated data without header. This is the ending for the file. The filename itself
is taken from the filename. e.g. if samp1.txt is the filename and external_wave='.wav'
then the program searches samp1.wav for the wavelength. The transpose setting is applied
and sets where the wavelength are to be inserted (columns or row indexes)
If use_same_name = False
this should be a full filename that contains the vector
use_same_name : bool, optional
this switches if the external filename included the loaded filename or is a separate file
correct_ascii_errors : bool (optional)
If True (Default) then the code tries to catch some stuff like double minus signs and double dots
'''
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep, index_col=0)
if correct_ascii_errors:
if (self.ds_ori.applymap(type) == float).all().all():
pass#all columns were converted to float,nice
else:
print('some data bad, try filtering')
try:# try forced conversion
self.ds_ori=self.ds_ori.applymap(lambda x: re.sub('--', '-',x) if type(x) is str else x)
self.ds_ori=self.ds_ori.applymap(lambda x: re.sub(r'\.+', '.',x) if type(x) is str else x)
self.ds_ori=self.ds_ori.astype(np.float64)
except Exception as e:
print('force cleaning went wrong and the file %s can not be read. Error message is:'%self.filename)
print(e)
return False
if external_time is not None:
if use_same_name:
time_file=check_folder(path=self.path,filename=self.filename.split('.')[0]+'.'+external_time)
else:
time_file=check_folder(path=self.path,filename=external_time)
if external_wave is not None:
if use_same_name:
wave_file=check_folder(path=self.path,filename=self.filename.split('.')[0]+'.'+external_wave)
else:
wave_file=check_folder(path=self.path,filename=external_wave)
if external_time is not None:
data_file_name=check_folder(path=self.path,filename=self.filename)
times=pandas.read_csv(time_file,header=None,decimal=decimal).values.ravel()
if transpose:
if external_wave is not None:
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep , decimal=decimal, header=None)
waves=pandas.read_csv(wave_file,header=None,decimal=decimal).values.ravel()
self.ds_ori.index=waves
else:
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep , decimal=decimal, index_col=0,header=None)
self.ds_ori.columns=times
else:
if external_wave is not None:
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep, decimal=decimal,header=None)
waves=pandas.read_csv(wave_file,header=None,decimal=decimal).values.ravel()
self.ds_ori.columns=waves
else:
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep, decimal=decimal)
self.ds_ori.index=times
elif external_wave is not None:
data_file_name=check_folder(path=self.path,filename=self.filename)
waves=pandas.read_csv(wave_file,header=None,decimal=decimal).values.ravel()
if transpose:
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep, decimal=decimal)
self.ds_ori.index=waves
else:
self.ds_ori=pandas.read_csv(check_folder(path=self.path,filename=self.filename), sep=sep, decimal=decimal,index_col=0,header=None)
self.ds_ori.columns=waves
self.ds_ori.columns=self.ds_ori.columns.astype('float')#Make columns indexes numbers
self.ds_ori.index=self.ds_ori.index.astype('float')#Make row indexes numbers
if index_is_energy:
self.ds_ori.index=scipy.constants.h*scipy.constants.c/(self.ds_ori.index*1e-9*scipy.constants.electron_volt)
if transpose:
self.ds_ori=self.ds_ori.T
if sort_indexes:
self.ds_ori.sort_index(axis=0,inplace=True)
self.ds_ori.sort_index(axis=1,inplace=True)
if shift_times_by is not None:
self.ds_ori.index=self.ds_ori.index.values+shift_times_by
if divide_times_by is not None:
self.ds_ori.index=self.ds_ori.index.values/divide_times_by
if data_type is not None:
self.data_type = data_type
if units is not None:
self.units = units
if baseunit is not None:
self.baseunit = baseunit
def __make_standard_parameter(self):
'''function that sets the standard parameter. The function takes no input, but we use this docstring to explain the parameter.
Parameters
-------------
log_scale : bool, optional
If False (Default), The 2D plots (Matrix) is plotted with a pseudo logarithmic intensity scale.
This usually does not give good results unless the intensity scale is symmetric
self.cmap : matplotlib.cm
(Default) standard_map - global parameter
cmap is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
self.lintresh : float
The pseudo logratihmic range "symlog" is used for most time axis. Symlog plots a range around
time zero linear and beyond this linear treshold 'lintresh' on a logarithmic scale. (Default) 0.3
self.log_fit :
(Default) False\n
Transfer all the time-fitting parameters into log-space before the fit
self.ignore_time_region : None or list (of two floats or of lists)
(Default) None
cut set a time range with a low and high limit from the fits. (Default) None nothing happens
The region will be removed during the fitting process (and will be missing in the fit-result
plots)\n
Usage single region: [lower region limit,upper region limit]\n
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
self.error_matrix_amplification :
(Default) 10
self.rel_wave : float or list (of floats)
(Default) np.arange(300,1000,100)\n
'rel_wave' and 'width' (in the object called 'wavelength_bin' work together for the creation
of kinetic plots. When plotting kinetic spectra one line will be plotted for each entrance
in the list/vector rel_wave. During object generation the vector np.arange(300,1000,100)
is set as standard. Another typical using style would be to define a list of interesting
wavelength at which a kinetic development is to be plotted. At each selected wavelength
the data between wavelength+ta.wavelength_bin and wavelength-ta.wavelength_bin is averaged
for each timepoint returned
self.rel_time : float or list/vector (of floats)
(Default) [0.2,0.3,0.5,1,3,10,30,100,300,1000,3000,9000]\n
For each entry in rel_time a spectrum is plotted. If time_width_percent=0 (Default) the
nearest measured timepoint is chosen. For other values see 'time_width_percent'
self.time_width_percent : float
(Default) 0 "rel_time" and "time_width_percent" work together for
creating spectral plots at specific timepoints. For each entry
in rel_time a spectrum is plotted. If however e.g. time_width_percent=10
the region between the timepoint closest to the 1.1 x timepoint
and 0.9 x timepoint is averaged and shown (and the legend adjusted accordingly).
This is particularly useful for the densly
sampled region close to t=0. Typically for a logarithmic recorded kinetics, the
timepoints at later times will be further appart than 10 percent of the value,
but this allows to elegantly combine values around time=0 for better statistics.
This averaging is only applied for the plotting function and not for the fits.
self.baseunit : str
(Default) 'ps'\n
baseunit is a neat way to change the unit on the time axis of the plots. (Default) 'ps', but they
can be frames or something similarly. This is changing only the label of the axis.
During the import there is the option to divide the numbers by a factor.
I have also used frames or fs as units. Important is that all time units will be labeled with
this unit.
self.mod :
(Default) 'exponential'\n
This is the default fitting function, in general this is discussed in the fitting section
self.scattercut : None or iterable (of floats or other iterable, always pairs!)
(Default) None\n
intented to "cut" one or multiple scatter regions. (if (Default) None nothing
happens) If it is set the spectral region between the limits is set to zero.
Usage single region: [lower region limit,upper region limit],
use for multiple regions:[[lower limit 1,upper limit 1],[lower limit 2,upper limit 2],...]
self.bordercut : None or iterable (with two floats)
(Default) None\n
cut spectra at the low and high wavelength limit. (Default) None
uses the limits of measurement
self.time_bin : None or int
(Default) None
is dividing the points on the time-axis in even bins and averages the found values in between.
This is a hard approach that also affects the fits. I do recommend to use this carefully,
it is most useful for modulated data. A better choice for transient absorption that only
affects the kinetics is 'time_width_percent'
self.timelimits : None or list (of 2 floats)
(Default) None\n
cut times at the low and high time limit. (Default) None uses the limits of measurement
Important: If either the background or the chirp is to be fit this must include the
time before zero! Useful: It is useful to work on different regions, starting with
the longest (then use the ta.Backgound function prior to fit) and expand from there
data_type : str
this is the datatype and effectively the unit put on the intensity axis
(Default)'differential Absorption in $\mathregular{\Delta OD}$
self.wave_nm_bin : None or float
(Default) None\n
rebins the original data into even intervals. If set to None the original data will be used.
If set to a width (e.g. 2nm), the wavelength axis will be divided into steps of this size
and the mean of all measurements in the interval is taken. The re-binning stops as soon as
the measured stepsize is wider than given here, then the original bins are used.
This function is particularly useful for spectrometer with non-linear dispersion,
like a prism in the infrared.
self.wavelength_bin : float, optional
(Default) 10nm the width used in kinetics, see below
self.intensity_range : None, float or list [of two floats]
(Default) None - intensity_range is a general switch that governs what intensity range the plots show.
For the 1d plots this is the y-axis for the 2d-plots this is the colour scale.
This parameter recognizes three settings. If set to "None" (Default) this uses the minimum and
maximum of the data. A single value like in the example below and the intended use is the symmetric
scale while a list with two entries an assymmetric scale e.g.
intensity_range=3e-3 is converted into intensity_range=[-3e-3,3e-3]
self.ds_ori.columns.name : str, optional
(Default) 'Wavelength in nm'\n
This is the general energy axis. here we define it with the unit. Change this to energy for use in e.g x-ray science
self.ds_ori.index.name : str, optional
Standard 'Time in %s' % self.baseunit
self.data_type: str (optional)
self.data_type='diff. Absorption in $\mathregular{\Delta OD}$'
self.fitcoeff : list (5 floats)
chirp correction polynom
self.chirp_file : str
if there is a file withthe right name write it here, otherwise None
self.figure_path : str
Path for saving figures, if set
self.save_figures_to_folder : bool
if True all figures are automatically saved when any plotfunction is called
Examples
-----------
>>> ta.bordercut=[350,1200] #remove all data outside this limit
>>> ta.scattercut=[522,605] #set data inside this limit to zero
>>> ta.timelimits=[0.2,5000] #remove all data outside this limit
>>> ta.wave_nm_bin=5 #rebin the data to this width
>>> ta.intensity_range=3e-3 #equivalent to [-3e-3,3e-3]
>>> ta.intensity_range=[-1e-3,3e-3] #intensity that is plotted in 2d plot and y-axis in 1d plots
>>> ta.cmap=matplotlib.cm.prism #choose different colour map
>>> ta.ignore_time_region=[-0.1,0.1] #ignore -0.1ps to 0.1ps
'''
self.log_scale = False if not hasattr(self, 'log_scale') else self.log_scale
self.cmap = standard_map if not hasattr(self, 'cmap') else self.cmap
self.lintresh = 0.3 if not hasattr(self, 'lintresh') else self.lintresh
self.log_fit = False if not hasattr(self, 'log_fit') else self.log_fit
self.ignore_time_region = None if not hasattr(self, 'ignore_time_region') else self.ignore_time_region
self.error_matrix_amplification = 10 if not hasattr(self, 'error_matrix_amplificatio') else self.error_matrix_amplification
try:
self.rel_wave = self.rel_wave
except:
self.rel_wave = np.arange(300,1000,100) if not hasattr(self, 'rel_wave') else self.rel_wave
self.rel_time = [0.2,0.3,0.5,1,3,10,30,100,300,1000,3000,9000] if not hasattr(self, 'rel_time') else self.rel_time
self.time_width_percent = 0 if not hasattr(self, 'time_width_percent') else self.time_width_percent
self.baseunit = 'ps' if not hasattr(self, 'baseunit') else self.baseunit
self.mod = 'exponential' if not hasattr(self, 'mod') else self.mod
self.scattercut = None if not hasattr(self, 'scattercut') else self.scattercut
self.bordercut = None if not hasattr(self, 'bordercut') else self.bordercut
self.time_bin = None if not hasattr(self, 'time_bin') else self.time_bin
self.timelimits = None if not hasattr(self, 'timelimits') else self.timelimits
self.wave_nm_bin = None if not hasattr(self, 'wave_nm_bin') else self.wave_nm_bin
self.wavelength_bin = 10 if not hasattr(self, 'wavelength_bin') else self.wavelength_bin
self.save_figures_to_folder = False if not hasattr(self, 'save_figures_to_folder') else self.save_figures_to_folder
self.intensity_range = None if not hasattr(self, 'intensity_range') else self.intensity_range
self.ds_ori.index.name = 'Time in %s' % self.baseunit if not hasattr(self, 'ds_ori.index.name') else self.ds_ori.index.name
self.equal_energy_bin = None if not hasattr(self, 'equal_energy_bin') else self.equal_energy_bin
self.units='nm' if not hasattr(self, 'units') else self.units
if self.units == 'nm':
self.ds_ori.columns.name = 'Wavelength in %s'%self.units if not hasattr(self, 'ds_ori.columns.name') else self.ds_ori.columns.name
elif self.units == 'eV':
self.ds_ori.columns.name = 'Energy in %s'%self.units if not hasattr(self, 'ds_ori.columns.name') else self.ds_ori.columns.name
elif self.units == 'keV':
self.ds_ori.columns.name = 'Energy in %s'%self.units if not hasattr(self, 'ds_ori.columns.name') else self.ds_ori.columns.name
else:
self.ds_ori.columns.name = 'Energy in %s'%self.units if not hasattr(self, 'ds_ori.columns.name') else self.ds_ori.columns.name
self.data_type= 'diff. Absorption in $\mathregular{\Delta OD}$' if not hasattr(self, 'data_type') else self.data_type
try:#self.fitcoeff
self.fitcoeff
if len(list(self.fitcoeff))<5:raise
except:
self.fitcoeff=[0,0,0,0,0] #: test comment here
try:#self.chirp_file
self.chirp_file
except:
if os.path.isfile(check_folder(path=self.path,filename=self.filename.split('.')[0] + '_chirp.dat')):
self.chirp_file=self.filename.split('.')[0] + '_chirp.dat'
else:
self.chirp_file=None
try:#self.figure_path
self.figure_path
except:
if self.save_figures_to_folder:
self.figure_path=check_folder(path="result_figures",current_path=path)
else:
self.figure_path=None
self.ds=self.ds_ori.copy()
[docs] def Filter_data(self, ds=None, cut_bad_times = False, replace_bad_values = 0, value = 20, uppervalue = None, lowervalue = None, upper_matrix = None, lower_matrix = None):
'''Filteres the data by applying hard replacements. if both replace_bad_values and
cut_bad_times are false or None, the times above "value" are replaced by zero
Parameters
------------
ds : pandas Dataframe, optional
if this is None (default) then the self.ds and self.ds_ori wil be filtered
value : float, optional
all values above this (absolute) value are considered to be corrupted. (Default 20) as classically the setup
reports optical DEnsity, an OD of 20 would be far above the typically expected
OD 1e-3. Pascher instrument software uses a value of 21 to indicate an error.
uppervalue : float, optional
all values above this number are considered to be corrupted. (Default 20) as classically the setup
reports optical DEnsity, an OD of 20 would be far above the typically expected
OD 1e-3. Pascher instrument software uses a value of 21 to indicate an error.
lowervalue : float, optional
all values below this number are considered to be corrupted. (Default -20) as classically the setup
reports optical DEnsity, an OD of -20 would be far above the typically expected
OD 1e-3. Pascher instrument software uses a value of 21 to indicate an error.
replace_bad_values : None of float, optional
values above the treshold are replaced with this value. Ignored of None (Default)
cut_bad_times = bool, optional
True (Default=False) removes the whole time where this is true
upper_matrix : Pandas DataFrame, optional
all values above this treshold will be put N/A or replace by the value in replace_bad_values
lower_matrix Pandas DataFrame, optional
all values below this treshold will be put N/A or replace by the value in replace_bad_values
the value is the upper bound. everything
above will be filtered. Standard is to drop the rows(=times) where something went wrong
Examples
---------
typical usage
>>> import plotfunc as pf
>>> ta=pf.TA('testfile.SIA')
>>> ta.Filter_data()
>>> ta.Filter_data(value=1) #to filter times with at least one point with OD 1
'''
if uppervalue is None: uppervalue = np.abs(value)
if lowervalue is None: lowervalue = -np.abs(value)
if replace_bad_values is not None:
cut_bad_times=False
if ds is None:
filtering=[self.ds,self.ds_ori]
else:
filtering=[ds]
for dataset in filtering:
if any([self.ignore_time_region is not None, self.scattercut is not None, self.bordercut is not None, self.timelimits is not None]):
dataset=sub_ds(dataset, ignore_time_region = self.ignore_time_region, scattercut = self.scattercut, bordercut = self.bordercut, timelimits = self.timelimits)
if cut_bad_times: #timepoint filter, delete the timepoints where value is stupid
matrix_size=len(dataset.index.values)
if upper_matrix is None:
damaged_times=dataset[np.any(dataset.values>uppervalue,axis=1)].index
else:
damaged_times=dataset[np.any(dataset.values>upper_matrix,axis=1)].index
dataset.drop(damaged_times,inplace = True)
if lower_matrix is None:
damaged_times=dataset[np.any(dataset.values<lowervalue,axis=1)].index
else:
damaged_times=dataset[np.any(dataset.values<lower_matrix,axis=1)].index
dataset.drop(damaged_times,inplace = True)
if len(dataset.index.values)<matrix_size*0.8:
print('attention, more than 20% of the data was removed by this filter.')
print('Please check with if the spectal borders contain regions without light (and high noise)')
print('Setting a bordercut and scattercut before the filtering might be useful')
else:
if replace_bad_values is None: #individual data filter
replace_bad_values=np.nan
if upper_matrix is None:
dataset.values[dataset.values>uppervalue]=replace_bad_values
else:
dataset.values[dataset.values>upper_matrix]=replace_bad_values
if lower_matrix is None:
dataset.values[dataset.values<lowervalue]=replace_bad_values
else:
dataset.values[dataset.values<lower_matrix]=replace_bad_values
if replace_bad_values == np.nan:
if dataset.isna().sum().sum()>0.2 * dataset.notna().sum().sum():
print('attention, more than 20% of the data was removed by this filter.')
print('Please check with if the spectal borders contain regions without light (and high noise)')
print('Setting a bordercut and scattercut before the filtering might be useful')
else:
if dataset[dataset==replace_bad_values].notna().sum().sum()> 0.2* dataset[dataset!=replace_bad_values].notna().sum().sum():
print('attention, more than 20% of the data was removed by this filter.')
print('Please check with if the spectal borders contain regions without light (and high noise)')
print('Setting a bordercut and scattercut before the filtering might be useful')
if ds is not None:return filtering[0]
[docs] def Background(self, lowlimit=None,uplimit=-1, use_median=False, ds=None, correction=None):
'''This is the background correction. In general it for each measured
wavelength averages the values from 'lowlimit' to 'uplimit' and
subtracts it from the data. It rund on the object (global) or if
given a specific ds local.
The low and uplimit can be set anywhere to substract any background.
It is important to note that many problems during measurements might
be visible in the data before time zero. So I recommend to first
plot without background correction and only after this inspection
apply the background correction.
The fit function has its own way to calculcate and apply a background
That could be used instead (but making the fit less stable)
Parameters
------------
lowlimit : None or float, optional
this is the lower limit from which the average (or median) is taken
(Default) is None, in which case the lower limit of the data is used.
uplimit : None or float, optional
this is the upper limit until which the average (or median) is taken
(Default) is -1 (usually ps), in which case the lower limit of the data is used.
use_median : bool, optional
the Median is a more outlier resistant metric in comparision to
the Mean (Average). However the values are not quite as close
to the distribution center in case of very few values. False
(Default) means the Mean is used
ds : None or DataFrame, optional
if None (Default) the internal Dataframe self.ds is used,
otherwise the pandas DataFrame ds is corrected and returned
correction : None or DataFrame, optional
this is the correction applied. It must be a DataFrame with
the same numbers of columns (spectral points) as the used ds
Examples
--------
if the object self has the name "ta"
typical useage:
>>> ta.Background()
specify inegrated are to - inf (Default) up to -0.5ps and use the Median for computation
>>> ta.Background(uplimit = -0.5, use_median = True)
'''
if ds is None:
run_global=True
ds=self.ds
else:
run_global=False
if correction is None:raise ValueError('We must have correction given, to slow otherhwise')
if (lowlimit is None) and (correction is None):
if use_median:
correction=ds[:uplimit].median(axis=0)
else:
correction=ds[:uplimit].mean(axis=0)
elif (lowlimit is not None) and (correction is None):
if use_median:
correction=ds[lowlimit:uplimit].median(axis=0)
else:
correction=ds[lowlimit:uplimit].mean(axis=0)
if run_global:
self.ds=ds-correction
self.background_par=[lowlimit,uplimit,use_median,correction]
else:
return ds-correction
[docs] def Man_Chirp(self,shown_window=[-1,1],path=None,max_points=40,cmap=cm.prism,ds=None):
'''Triggering of Manuel Fix_Chirp. usually used when Cor_Chirp has run already.
Alternatively delete the chirp file. This Function opens a plot in which the user manually selects a number of points
These points will then be interpolated with a 4th order polynomial
The user can then select a new t=0 point.
The first option allows to fine select an intensity setting for this chirp correction.
However sometimes spikes are making this things difficult. In this case set a guessed intensity with self.intensity_range=1e-3
Parameters
-------------
path : str or path object (optional)
if path is a string without the operation system dependent separator, it is treated as a relative path,
e.g. data will look from the working directory in the sub director data. Otherwise this has to be a
full path in either strong or path object form.
shown_window : list (with two floats), optional
Defines the window that is shown during chirp correction. If the t=0 is not visible, adjust this parameter
to suit the experiment. If problems arise, I recomment to use Plot_Raw to check where t=0 is located
max_points : int, optional
Default = 40 max numbers of points to use in Gui selection. Useful option in case no middle mouse button
is available. (e.g. touchpad)
cmap : matplotlib colourmap, optional
Colourmap to be used for the chirp correction. While there is a large selection here I recommend to choose
a different map than is used for the normal 2d plotting.\n
cm.prism (Default) has proofen to be very usefull
ds: pandas dataframe,optional
this allows to hand in an external ds, if this is done then the on disk saved fitcoeff are the new ones only and the
function returns the new fitcoeff and the combined fitcoeff, self also has a new variable called self.combined_fitcoeff
the original file on dis and self.fitcoeff are NOT overwritten (are the old ones)
the self.ds is the NEW one (with the correction applied)
to reverse simply run Cor_Chirp()
to permanently apply change self.fitcoeff with self.combined_fitcoeff and rename the file with 'filename_second_chirp' to filename_chirp
'''
if ds is None:
ds=self.ds_ori
original=True
else:
original=False
if original:
temp_ds = Fix_Chirp(ds, cmap = cmap, save_file = None, intensity_range = self.intensity_range,
wave_nm_bin = 10, shown_window = shown_window, filename = self.filename,
scattercut = self.scattercut, bordercut = self.bordercut,
path = check_folder(path = path, current_path = self.path), max_points = max_points)
else:
temp_ds = Fix_Chirp(ds, cmap = cmap, save_file = None, intensity_range = self.intensity_range,
wave_nm_bin = 10, shown_window = shown_window, filename = self.filename+'_second_chirp',
scattercut = self.scattercut, bordercut = self.bordercut,
path = check_folder(path = path, current_path = self.path), max_points = max_points)
if isinstance(temp_ds,pandas.DataFrame):
self.ds=temp_ds
self.chirp_file=self.filename.split('.')[0] + '_chirp.dat'
if original:#we have run from scratch
self.Cor_Chirp(path=path)
else:
print('you provided a separate ds file. returned are the new fitcoeff and the combined fitcoeff, ta also has a new variable called ta.combined_fitcoeff')
save_file=check_folder(path=path,current_path = self.path, filename=self.filename+'_second_chirp')
with open(save_file,'r') as f:
new_fitcoeff=f.readline()
new_fitcoeff=np.array(new_fitcoeff.split(','),dtype='float')
self.combined_fitcoeff=self.fitcoeff+new_fitcoeff
return new_fitcoeff,self.combined_fitcoeff
else:
raise Warning('Man Chirp interrupted')
def Check_Chirp(self, chirp_file = None, fitcoeff = None, cmap = cm.prism, path=None, ds=None,
shown_window = [-1, 1]):
'''*Check_Chirp* is a function to check a provided chirp correction. It is is intended as
an option when Cor_Chirp fails due to lacking GUI.
A 4th order polynomial is plotted over the data, printed and returned.
The intended use is that the components of this polynomial can be adjusted and then handed to
Cor_chirp.
If neither a chirp_file not fitcoeff are provided a general purpose chrip correction is suggested
as starting value. The function does not alter anything.
use the internal intensity range to adjust for an optimum representation. ta.intensity_range=1e-3
Parameters
-------------
chirp-file : None or str, optional
If a raw file was read(e.g. "data.SIA") and the chirp correction was
completed, a file with the attached word "chirp" is created and
stored in the same location. ("data_chirp.dat") This file contains
the 5 values of the chirp correction. By selecting such a file
(e.g. from another raw data) a specific chirp is applied. If a
specific name is given with **chirp_file** (and optional **path**)
then this file is used.\n
GUI\n
The word *'gui'* can be used instead of a filename to open a gui that
allows the selection of a chrip file
path : str or path object (optional)
if path is a string without the operation system dependent separator, it is treated as a relative path,
e.g. data will look from the working directory in the sub director data. Otherwise this has to be a
full path in either strong or path object form.
shown_window : list (with two floats), optional
Defines the window that is shown during chirp correction. If the t=0 is not visible, adjust this parameter
to suit the experiment. If problems arise, I recomment to use Plot_Raw to check where t=0 is located
fitcoeff : list or vector (5 floats), optional
One can give a vector/list with 5 numbers representing the parameter
of a 4th order polynomial (in the order
:math:`(a4*x^4 + a3*x^3+a2*x^2+a1*x1+a0)`.
cmap : matplotlib colourmap, optional
Colourmap to be used for the chirp correction. While there is a large selection here I recommend to choose
a different map than is used for the normal 2d plotting.\n
cm.prism (Default) has proofen to be very usefull
Examples
----------
In most cases:
>>> import plot_func as pf
>>> ta = pf.TA('test1.SIA') #open the original project,
>>> fitcoeff=ta.Check_Chirp() # if no specific correction is found
>>> fitcoeff[1]+=0.1e-8
>>> ta.Check_Chirp(fitcoeff=fitcoeff)
'''
if ds is None:ds=self.ds
if (chirp_file is None) and (fitcoeff is None):
fitcoeff=[-3.384e-12,1.456e-08,-2.366e-05,0.0172,-4.306]
elif chirp_file is not None:
if 'gui' in chirp_file:
root_window = tkinter.Tk()
root_window.withdraw()
root_window.attributes('-topmost',True)
root_window.after(1000, lambda: root_window.focus_force())
complete_path = filedialog.askopenfilename(initialdir=os.getcwd())
listen=os.path.split(complete_path)
path=os.path.normpath(listen[0])
chirp_file=listen[1]
path=check_folder(path,self.path)
with open(check_folder(path=path,filename=chirp_file),'r') as f:
fitcoeff=[float(a) for a in f.readline().split(',')]
fig,ax=plt.subplots()
ax = plot2d(ax = ax, ds = ds, cmap = cmap, wave_nm_bin = self.wave_nm_bin, scattercut = self.scattercut, bordercut = self.bordercut,
timelimits = shown_window, intensity_range = self.intensity_range,
title = 'This plot shows the set chirp', use_colorbar = False,
plot_type = "linear", log_scale = False)
correcttimeval = np.polyval(fitcoeff, ds.columns.values.astype('float'))
ax.plot(ds.columns.values.astype('float'),correcttimeval)
print('used fitcoeff:')
print(fitcoeff)
return fitcoeff
[docs] def Cor_Chirp(self, chirp_file = None, path = None, shown_window = [-1, 1], fitcoeff = None, max_points = 40, cmap = cm.prism):
'''*Cor_Chirp* is a powerful Function to correct for a different arrival times of
different wavelength (sometimes call chirp).
In general if a file is opened for the first time this function is opening
a plot and allows the user to select a number of points, which are then
approximated with a 4th order polynomial and finally to select a point
that is declared as time zero. The observed window as well as the intensities
and the colour map can be chosen to enable a good correction. Here a fast
iterating colour scheme such as "prism" is often a good choice. In all of the
selections a left click selects, a right click removes the last point and
a middle click (sometime appreviated by clicking left and right together)
finishes the selection. If no middle click exists, the process
automatically ends after max_points (40 preset).
The first option allows to fine select an intensity setting for this chirp correction.
However sometimes spikes are making this things difficult.
In this case set a guessed intensity with self.intensity_range=1e-3\n
Note that scattercut, bordercut and intensity_range can be used
After the first run the polynom is stored in self.fitcoeff, a new matrix
calculated from self.ds_ori that is stored as self.ds and a file stored in the
same location as the original data. The second time the function *Cor_Chirp* is
run the function will find the file and apply the chirp correction automatically.
If one does want to re-run the chirp correction the function *Man_Chirp* does
not look for this file, but creates after finishing a new file.
Alternatively the polynom or a filename can be given that load a chirp correction
(e.g. from a different run with the same sample).
The function *Cor_Chirp* selects in the order:
# "fitcoeff"
# "other files"
# "stored_file"
# call Man_Chirp (clicking by hand)
Parameters
-------------
chirp-file : None or str, optional
If a raw file was read(e.g. "data.SIA") and the chirp correction was
completed, a file with the attached word "chirp" is created and
stored in the same location. ("data_chirp.dat") This file contains
the 5 values of the chirp correction. By selecting such a file
(e.g. from another raw data) a specific chirp is applied. If a
specific name is given with **chirp_file** (and optional **path**)
then this file is used.\n
GUI\n
The word *'gui'* can be used instead of a filename to open a gui that
allows the selection of a chrip file
path : str or path object (optional)
if path is a string without the operation system dependent separator, it is treated as a relative path,
e.g. data will look from the working directory in the sub director data. Otherwise this has to be a
full path in either strong or path object form.
shown_window : list (with two floats), optional
Defines the window that is shown during chirp correction. If the t=0 is not visible, adjust this parameter
to suit the experiment. If problems arise, I recomment to use Plot_Raw to check where t=0 is located
fitcoeff : list or vector (5 floats), optional
One can give a vector/list with 5 numbers representing the parameter
of a 4th order polynomial (in the order
:math:`(a4*x^4 + a3*x^3+a2*x^2+a1*x1+a0)`. The chirp parameter are
stored in ta.fitcoeff and can thus be used in other TA objects. This
vector is also stored with the file and automatically applied during
re-loading of a hdf5-object
max_points : int, optional
Default = 40 max numbers of points to use in Gui selection. Useful option in case no middle mouse button
is available. (e.g. touchpad)
cmap : matplotlib colourmap, optional
Colourmap to be used for the chirp correction. While there is a large selection here I recommend to choose
a different map than is used for the normal 2d plotting.\n
cm.prism (Default) has proofen to be very usefull
Examples
----------
In most cases:
>>> import plot_func as pf
>>> ta = pf.TA('test1.SIA') #open the original project,
>>> ta.Cor_Chirp()
Selecting a specific correction
>>> ta.Cor_Chirp(‘gui’)
>>> ta.Cor_Chirp(chirp_file = 'older_data_chirp.dat')
>>> #use the coefficients from a different project
>>> ta.Cor_Chirp(fitcoeff = ta_old.fitcoeff) #use the coefficients from a different project
'''
if chirp_file is None:
chirp_file=self.chirp_file
elif 'gui' in chirp_file:
root_window = tkinter.Tk()
root_window.withdraw()
root_window.attributes('-topmost',True)
root_window.after(1000, lambda: root_window.focus_force())
complete_path = filedialog.askopenfilename(initialdir=os.getcwd())
listen=os.path.split(complete_path)
path=os.path.normpath(listen[0])
chirp_file=listen[1]
path=check_folder(path,self.path)
if fitcoeff is not None:#we use a stored project
try:
if len(fitcoeff)==5 or len(fitcoeff)==6:#we provide a valid list/vector
if all(elem == 0 for elem in fitcoeff):
self.ds=self.ds_ori
print('all chirp coefficients are zero so no chirp correction applied')
else:
self.ds=Fix_Chirp(self.ds_ori,fitcoeff=fitcoeff)
self.fitcoeff=fitcoeff #we came to here so fitcoeff must be right
else:
raise
except:
self.ds=self.ds_ori
print('something went wrong with the provided fitcoeff. This should be either a list/array with 5-6 parameter or the object should contain the parameter')
print('fitcoeff is currently:' + fitcoeff)
else:
try:
if chirp_file is None:
save_file=None
else:
save_file=check_folder(path = path, filename = chirp_file)
self.ds = Fix_Chirp(self.ds_ori, cmap = cmap, save_file = save_file,
filename = self.filename, path = self.path,
scattercut = self.scattercut, bordercut = self.bordercut,
intensity_range = self.intensity_range, wave_nm_bin = 10, shown_window = shown_window,
fitcoeff = fitcoeff, max_points = max_points)
if save_file is None:
if self.filename is None:
chirp_file='chirp.dat'
else:
f=self.filename.split('.')[0]
chirp_file=f+'_chirp' + '.dat'
self.chirp_file=chirp_file
with open(check_folder(path=path,filename=chirp_file),'r') as f:
self.fitcoeff=[float(a) for a in f.readline().split(',')]
except:
print(check_folder(path=self.path,filename=self.filename.split('.')[0] + '_chirp.dat'))
if os.path.isfile(check_folder(path=self.path,filename=self.filename.split('.')[0] + '_chirp.dat')):
print('somehting is wrong, try deleting old chirp file')
raise
else:
print('No old chirp file')
self.Man_Chirp(path=path,cmap=cmap,shown_window=shown_window,max_points=max_points)
chirp_file=self.chirp_file
with open(check_folder(path=path,filename=chirp_file),'r') as f:
self.fitcoeff=[float(a) for a in f.readline().split(',')]
self.ds.columns.name=self.ds_ori.columns.name
self.ds.index.name=self.ds_ori.index.name
def Plot_Interactive(self, fitted = False, ds = None, cmap = None, plot_on_move = False):
'''interactive plotting function. it plots the matrix in the middle and two slices that are selected by the mouse (click)
Parameters
---------------
fitted : bool, optional
this switch decides if the fitted or the RAW data is plotted with this widget to
inspect the data data. If fitted is False (Default) then the raw data and an interpolation
is used to plot.
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
ds : DataFrame, optional
if None (Default), the program first tests self.ds and if this is not there then self.ds_ori.
This option was introduced to allow plotting of other matrixes with the same parameter
plot_on_move : bool, optional
Default: False plots the slices after click, on True the plot constantly reslices and on click
The current position is written down.
'''
from matplotlib.widgets import Cursor
if cmap is None:cmap=self.cmap
if ds is None:
if not fitted:
if self.ds is None:
ds=self.ds_ori.copy()
else:
ds=self.ds.copy()
else:
ds=self.re['A']
modelled=self.re['AC']
intensity_range=self.intensity_range
if intensity_range is None:
try:
maxim=max([abs(ds.values.min()),abs(ds.values.max())])
intensity_range=[-maxim,maxim]
except:
intensity_range=[-1e-2,1e-2]
else:
if not hasattr(intensity_range,'__iter__'):#lets have an lazy option
intensity_range=[-intensity_range,intensity_range]
class MouseMove:
# initialization
def __init__(self, ds, cmap, intensity_range, log_scale, baseunit,
timelimits, scattercut, bordercut, wave_nm_bin, equal_energy_bin, ignore_time_region,
time_bin, lintresh, data_type, width, time_width_percent):
fig = plt.figure(tight_layout=True,figsize=(14,8))
gs = GridSpec(5, 4)
self.ax= fig.add_subplot(gs[1:, :3])
self.ds=ds
self.cmap=cmap
self.intensity_range=intensity_range
self.log_scale=log_scale
self.baseunit=baseunit
self.timelimits=timelimits
self.scattercut=scattercut
self.bordercut=bordercut
self.wave_nm_bin=wave_nm_bin
self.equal_energy_bin=equal_energy_bin
self.ignore_time_region=ignore_time_region
self.time_bin=time_bin
self.data_type=data_type
self.width=width
self.lintresh=lintresh
self.time_width_percent=time_width_percent
self.ax = plot2d(ds=ds, ax=self.ax, cmap=cmap, intensity_range=self.intensity_range,
log_scale=self.log_scale, baseunit=self.baseunit, timelimits=self.timelimits,
scattercut=self.scattercut, bordercut=self.bordercut, wave_nm_bin=self.wave_nm_bin, equal_energy_bin=self.equal_energy_bin,
ignore_time_region=self.ignore_time_region, time_bin=self.time_bin,
lintresh=self.lintresh, data_type = self.data_type, use_colorbar = False)
self.ax_time= fig.add_subplot(gs[0, :3],sharex=self.ax)
self.ax_kinetic= fig.add_subplot(gs[1:, -1],sharey=self.ax)
plt.subplots_adjust(wspace=0,hspace=0)
if plot_on_move:
fig.canvas.mpl_connect('motion_notify_event', self.move)
fig.canvas.mpl_connect('button_press_event', self.click)
else:
fig.canvas.mpl_connect('button_press_event', self.move)
def click(self, event):
x, y = event.xdata, event.ydata
if self.equal_energy_bin is not None:
x=scipy.constants.h*scipy.constants.c/(x*1e-9*scipy.constants.electron_volt)
print('x=%g, y=%g\n'%(x,y))
def move(self, event):
x, y = event.xdata, event.ydata
if self.equal_energy_bin is not None:
x=scipy.constants.h*scipy.constants.c/(x*1e-9*scipy.constants.electron_volt)
try:
self.ax_time.cla()
except:
pass
if not fitted:
ds_temp1 = sub_ds(ds = Frame_golay(ds,5,3), times = y, time_width_percent = self.time_width_percent,
scattercut = self.scattercut, drop_scatter=True, bordercut = self.bordercut,
ignore_time_region = self.ignore_time_region, wave_nm_bin = self.wave_nm_bin, equal_energy_bin=self.equal_energy_bin,
wavelength_bin = self.width)
ds_temp1.plot(ax=self.ax_time,style='-',color='red')
else:
ds_temp1 = sub_ds(ds = modelled, times = y, time_width_percent = self.time_width_percent,
scattercut = self.scattercut, drop_scatter=True, bordercut = self.bordercut,
ignore_time_region = self.ignore_time_region, wave_nm_bin = self.wave_nm_bin, equal_energy_bin=self.equal_energy_bin,
wavelength_bin = self.width)
ds_temp1.plot(ax=self.ax_time,style='-',color='red')
ds_temp = sub_ds(ds = ds, times = y, time_width_percent = self.time_width_percent,
scattercut = self.scattercut, drop_scatter=True, bordercut = self.bordercut,
ignore_time_region = self.ignore_time_region, wave_nm_bin = self.wave_nm_bin, equal_energy_bin=self.equal_energy_bin,
wavelength_bin = self.width)
ds_temp.plot(ax=self.ax_time,style='*',color='black')
self.ax_time.plot(self.ax_time.get_xlim(),[0,0],'gray')
if not fitted:
self.ax_time.legend(['%.3g %s smoothed'%(y,self.baseunit)])
else:
self.ax_time.legend(['%.3g %s fitted'%(y,self.baseunit)])
self.ax_time.set_yticks(self.ax_time.get_ylim())
self.ax_time.set_yticklabels(['%.1e'%f for f in self.ax_time.get_ylim()])
for i in range(3):
try:
self.ax_kinetic.lines.pop(0)
except:
pass
if self.width is None:
self.width = 10
if not fitted:
ds_temp1 = sub_ds(ds = Frame_golay(ds), wavelength = x, scattercut = self.scattercut, drop_scatter=True,
bordercut = self.bordercut, ignore_time_region = self.ignore_time_region,
wave_nm_bin = self.wave_nm_bin,wavelength_bin = self.width)
self.ax_kinetic.plot(ds_temp1.values,ds_temp1.index.values,'-',label='%.0f smoothed'%x,color='red')
else:
ds_temp1 = sub_ds(ds = modelled, wavelength = x, scattercut = self.scattercut, drop_scatter=True,
bordercut = self.bordercut, ignore_time_region = self.ignore_time_region,
wave_nm_bin = self.wave_nm_bin, wavelength_bin = self.width)
self.ax_kinetic.plot(ds_temp1.values,ds_temp1.index.values,'-',label='%.0f fitted'%x,color='red')
ds_temp = sub_ds(ds = ds, wavelength = x, scattercut = self.scattercut, drop_scatter=True,
bordercut = self.bordercut, ignore_time_region = self.ignore_time_region,
wave_nm_bin = self.wave_nm_bin, wavelength_bin = self.width)
self.ax_kinetic.set_xlim(min([0,min(ds_temp.values)]),max([max(ds_temp.values),0]))
self.ax_kinetic.plot(ds_temp.values,ds_temp.index.values,'*',label='%.0f'%x, color='black')
self.ax_kinetic.plot([0,0],self.ax_kinetic.get_ylim(),'gray')
self.ax_kinetic.legend(['%.0f'%x])
self.ax_kinetic.set_xticks(self.ax_kinetic.get_xlim())
self.ax_kinetic.set_xticklabels(['%.1e'%f for f in self.ax_kinetic.get_xlim()])
self.ax_kinetic.set_yticklabels(self.ax.get_yticklabels())
plt.subplots_adjust(wspace=0,hspace=0)
eve=MouseMove(ds, cmap, self.intensity_range, self.log_scale, self.baseunit, self.timelimits,
self.scattercut, self.bordercut, self.wave_nm_bin, self.equal_energy_bin, self.ignore_time_region,
self.time_bin, self.lintresh, self.data_type, self.wavelength_bin, self.time_width_percent)
cursor = Cursor(eve.ax, useblit=True, color='red', linewidth=2)
return eve,cursor
[docs] def Plot_RAW(self, plotting = range(4), title = None, scale_type = 'symlog', times = None,
cmap = None, filename = None, path = "result_figures", savetype = 'png' , print_click_position = False,
plot_second_as_energy = True, ds = None):
'''This is a wrapper function that triggers the plotting of various RAW (non fitted) plots.
The shaping parameter are taken from the object and should be defined before.
The parameter in this plot call are to control the general look and features of the plot.
Which plots are printed is defined byt the first command (plotting)
The plots are generated on the fly using self.ds and all the shaping parameter
In all plots the RAW data is plotted as dots and interpolated with lines
(using Savitzky-Golay window=5, order=3 interpolation). As defined by the internal parameters
at selected time-points and the kinetics for selected wavelength are shaped by the
object parameter. The SVD is performed using the same shaping parameter and is commonly
used as an orientation for the number of components in the data.
Everything is handed over to 'plot_raw' function that can be used for extended RAW plotting.
Parameters
---------------
plotting : int or iterable (of integers), optional
This parameter determines which figures are plotted
the figures can be called separately with plotting = 1
or with a list of plots (Default) e.g. plotting=range(4) calls plots 0,1,2,3.
The plots have the following numbers:
0. Matrix
1. Kinetics
2. Spectra
3. SVD
The plotting takes all parameter from the "ta" object.
title : None or str
title to be used on top of each plot
The (Default) None triggers self.filename to be used. Setting a specific title as string will.
be used in all plots. To remove the title all together set an empty string with this command title="" .
Scale_type : None or str
is a general setting that can influences what time axis will be used for the plots.
"symlog" (linear around zero and logarithmic otherwise) "lin" and "log" are valid options.
times : int
are the number of components to be used in the SVD (Default) is 6.
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
filename : str, optional
offers to replace the base-name used for all plots (to e.g.~specify what sample was used).
if (Default) None is used, the self.filename is used as a base name. The filename plays only a
role during saving, as does the path and savetype.
path : None or str or path object, optional
This defines where the files are saved if the safe_figures_to_folder parameter is True,
quite useful if a lot of data sets are to be printed fast.
If a path is given, this is used. If a string like the (Default) "result_figures" is given,
then a subfolder of this name will be used (an generated if necessary)
relative to self.path. Use and empty string to use the self.path
If set to None, the location of the plot_func will be used and
a subfolder with title "result_figures" be generated here.
savetype : str or iterable (of str), optional
matplotlib allows the saving of figures in various formats. (Default) "png",
typical and recommendable options are "svg" and "pdf".
print_click_position : bool, optional
if True then the click position is printed for the spectral plots
ds : DataFrame, optional
if None (Default), the program first tests self.ds and if this is not there then self.ds_ori.
This option was introduced to allow plotting of other matrixes with the same parameter
Examples
------------
Typically one would call this function empty for an overview. We name the object "ta" so with
>>> ta=pf.TA('testfile.SIA')
This would trigger the plotting of the 4 mayor plots for an overview.
>>> ta.Plot_RAW()
This would plot only the kinetics.
>>> ta.Plot_RAW(1)
>>> ta.Plot_RAW(plotting = 1)
'''
path=check_folder(path=path,current_path=self.path)
if self.save_figures_to_folder:
self.figure_path=path
if cmap is None:cmap=self.cmap
if ds is None:
if self.ds is None:
ds=self.ds_ori.copy()
else:
ds=self.ds.copy()
if filename is None: filename=self.filename
if not hasattr(plotting,"__iter__"):plotting=[plotting]
if title is None:
if filename is None:
title=self.filename
else:
title=filename
plot_raw(ds=ds, plotting=plotting, cmap=cmap, title=title, path=path, filename=filename,
intensity_range=self.intensity_range, log_scale=self.log_scale, baseunit=self.baseunit,
timelimits=self.timelimits, scattercut=self.scattercut, bordercut=self.bordercut,
wave_nm_bin=self.wave_nm_bin, rel_wave=self.rel_wave, width=self.wavelength_bin,
time_width_percent=self.time_width_percent, ignore_time_region=self.ignore_time_region,
time_bin=self.time_bin, rel_time=self.rel_time, save_figures_to_folder=self.save_figures_to_folder,
savetype=savetype,plot_type=scale_type,lintresh=self.lintresh, times=times,
print_click_position = print_click_position, data_type = self.data_type,
plot_second_as_energy = plot_second_as_energy, units=self.units, equal_energy_bin = self.equal_energy_bin)
[docs] def Save_Plots(self, path = 'result_figures', savetype = None, title = None, filename = None, scale_type = 'symlog',
patches = False, cmap = None):
'''Convenience function that sets save_plots_to_folder temporarily to true and replots everything
Parameters
----------
path : None, str or path, optional
(Default) None, if left on None, then a folder "result_figures" is created in the folder
of the data (self.path)
savetype : str or iterable (of str), optional
matplotlib allows the saving of figures in various formats. (Default) "png",
typical and recommendable options are "svg" and "pdf".
title : None or str, optional
(Default) None, Use this title on all plots. if None, use self.filename
filename : str, optional
(Default) None, Base name for all plots. If None, then self.filename will be used
scale_type : str, optional
"symlog" (Default), "linear", "log" time axis
patches : bool, optional
For true use white patches to label things in the 2d matrixes, to safe
space for publication
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
Examples
---------
>>> ta.Save_Plots()
>>> ta.Save_Plots(patches = True)
'''
if cmap is None:cmap=self.cmap
if savetype is None:
savetype=['png']
elif savetype in ['png','pdf','svg']:savetype=[savetype]
elif hasattr(savetype,"__iter__"):savetype=list(savetype)
else:
print('Please specify a single filetype from \'png\',\'pdf\',\'svg\' or a list of those. Nothing was saved')
return False
if cmap is None:cmap=standard_map
origin=self.save_figures_to_folder
self.save_figures_to_folder=True
try:
for t in savetype:
plt.close('all')
self.Plot_RAW(savetype = t, path = path, cmap = cmap, title = title,
scale_type = scale_type, filename = filename, units=self.units,
equal_energy_bin = self.equal_energy_bin)
plt.close('all')
print('saved RAW plots type %s to %s'%(t,check_folder(path=path,current_path=self.path)))
except:
print('Saving of Raw plots for filetype %s failed'%t)
try:
for t in savetype:
plt.close('all')
self.Plot_fit_output(savetype=t,path=path,cmap=cmap,title=title,scale_type=scale_type,patches=patches,filename=filename)
plt.close('all')
print('Saved Fit plots of type %s to %s'%(t,check_folder(path=path,current_path=self.path)))
except:
print('Saving of Fit plots for filetype %s failed'%t)
self.save_figures_to_folder=origin
def __Fit_Chirp_inner( self, opt_coeff, initial_fit_coeff = None, params = None, scattercut = None, bordercut = None,
timelimits = None, wave_nm_bin = None, time_bin = None, mod = None, log_fit = None, ds_back_corr = None):
''' Function to calculate a new chirp corrected matrix and return an error value, The "cost function" for the chirp optimization
'''
fitcoeff = np.array([opt_coeff['p4'].value, opt_coeff['p3'].value, opt_coeff['p2'].value, opt_coeff['p1'].value, opt_coeff['p0'].value])
#fitcoeff = __shift_polynom(fitcoeff, -500) #this was an arbitrary shift of the data by 500 to make the chirp parameter
time = ds_back_corr.index.values.astype('float')#extract the time
ds_new = ds_back_corr.apply(lambda x:np.interp(x = time+np.polyval(fitcoeff, x.name), xp = time, fp = x), axis = 0, raw = False)
re = err_func(paras = params, ds = ds_new, mod = mod, final = False, log_fit = log_fit)
return re
def __Fit_Chirp_outer(self, pardf, results, fit_ds, fit_chirp_iterations, mod, deep_iteration = False):
'''Broken out Chirp optimization, takes the fitted parameters and performs 'fit_chirp_iterations' times the loop,
(optimise chirp + optimize global) after each global iteration the error is compared to the previous. It continues until no improvement is made or
until the 'fit_chirp_iterations' is reached. If the error is reduced by more than a factor of 100 in a single step, it is assumed that something fishy is going on and we restart the fit, but with a 10x smaller simplex stepsize and deep_iteraction FAlse
Parameters
-----------
pardf
# deep_iteration uses the previous kinetic optimized parameter as the input into the next fit.
#Can be great but can also run away, in general not needed and can be triggered by feeding the
#results back into the global fit'''
if pardf.vary.any():
initial_error = [results.residual[0]]
par_into_chirpfit = results.params
else:
initial_error = [err_func(paras = self.par_fit, ds = fit_ds, mod = self.mod, final = False, log_fit = self.log_fit)]
par_into_chirpfit = self.par
par_into_chirpfit['t0'].vary = False
initial_fit_coeff = self.fitcoeff
if len(initial_fit_coeff) == 6:
initial_fit_coeff[4] = self.fitcoeff[4]+self.fitcoeff[5]
initial_fit_coeff = initial_fit_coeff[:5]
chirp_par = lmfit.Parameters()
for i in range(5):
chirp_par.add('p%i'%(4-i), value = initial_fit_coeff[i])
chirp_par['p4'].set(min = chirp_par['p4']-0.5, max = chirp_par['p4']+0.5)
try:#lets send in the background corrected matrix fails if no prior background was done
correction = self.background_par[3]
ds_back_corr = self.ds_ori-correction
except:
ds_back_corr = self.ds_ori
ds_back_corr = sub_ds(ds = ds_back_corr, scattercut = self.scattercut, bordercut = self.bordercut,
timelimits = self.timelimits, wave_nm_bin = self.wave_nm_bin, time_bin = self.time_bin,
equal_energy_bin = self.equal_energy_bin)
print('Before chirpfit the error is:{:.6e}'.format(initial_error[-1]))
#################################################################################################################
#----Chirp fit loop---------------------------------------------------------------------------------
#################################################################################################################
for loop in range(fit_chirp_iterations):
chirpmini = lmfit.Minimizer(self.__Fit_Chirp_inner, chirp_par,
fcn_kws = {'ds_back_corr':ds_back_corr.copy(), 'initial_fit_coeff':initial_fit_coeff,
'params':par_into_chirpfit, 'mod':self.mod, 'log_fit':self.log_fit,
'scattercut':self.scattercut, 'bordercut':self.bordercut,
'timelimits':self.timelimits, 'wave_nm_bin':self.wave_nm_bin,
'time_bin':self.time_bin})
step_size = 5e-2
try:
start = tm.time()
simp = np.array([chirp_par['p4'].value, chirp_par['p3'].value, chirp_par['p2'].value, chirp_par['p1'].value, chirp_par['p0'].value])
simp = np.tile(simp.reshape(5, 1), 6).T
for i in range(5):
if simp[i+1, i] != 0:
if i<4:
simp[i+1, i] = simp[i+1, i]*(step_size)
else:
simp[i+1, i] = simp[i+1, i]+0.1
else:
simp[i+1, i] = 1e-4
#we start by optimizing the chirp with fixed Global fit
chirp_results = chirpmini.minimize('nelder', options = {'maxfev':1e4, 'fatol':initial_error[-1]*1e-6, 'initial_simplex':simp})
end = tm.time()
opt_coeff = chirp_results.params
temp = np.array([opt_coeff['p4'].value, opt_coeff['p3'].value, opt_coeff['p2'].value, opt_coeff['p1'].value, opt_coeff['p0'].value])
#Create the new chirp corrected data
time = ds_back_corr.index.values.astype('float')#extract the time
new_ds = ds_back_corr.copy().apply(lambda x:np.interp(x = time+np.polyval(temp, float(x.name)), xp = time, fp = x), axis = 0, raw = False)
#New Global Fit
fit_ds_loop = sub_ds(ds = new_ds, scattercut = self.scattercut, bordercut = self.bordercut, timelimits = self.timelimits, wave_nm_bin = self.wave_nm_bin, equal_energy_bin = self.equal_energy_bin, time_bin = self.time_bin)
if pardf.vary.any():
mini = lmfit.Minimizer(err_func, par_into_chirpfit, fcn_kws = {'ds':fit_ds_loop, 'mod':mod, 'log_fit':self.log_fit, 'final':False})
results_in_chirp = mini.minimize('nelder', options = {'maxiter':1e5})
initial_error.append(results_in_chirp.residual[0])
else:
initial_error.append(err_func(paras = par_into_chirpfit, ds = fit_ds_loop, mod = mod, final = False, log_fit = self.log_fit))
if initial_error[-1]<initial_error[-2]:
if initial_error[-2]/initial_error[-1]>100:#something fishy going on. lets try again
print('Chirp_loop {:02d} strange decrease step size'.format(loop+1))
initial_error[-1] = initial_error[-2]
step_size = step_size/100
if len(initial_error)>4:
if initial_error[-4] == initial_error[-1]:#we have run this trick now three times, time to break
raise StopIteration
deep_iteration=False
else:
print('Chirp_loop {:02d} resulted in :{:.8e}'.format(loop+1, initial_error[-1]))
if deep_iteration: #This results in a very deep iteration of the starting parameter
if pardf.vary.any():
par_into_chirpfit = results_in_chirp.params
chirp_par = chirp_results.params
else:
raise StopIteration
except StopIteration:
print('iteration is not smaller finished chirp looping')
break
except:
print('failure in chirp optimisation in iteration %i'%(loop+1))
import sys
print("Unexpected error:", sys.exc_info()[0])
initial_error.append(initial_error[0])#to avoid that numbers are written
break
#################################################################################################################
#-----------------------------------------end chrip fit loop-------------------------------------------------
#################################################################################################################
if initial_error[-1]<initial_error[0]:#lets check if we improved anything
print('chirp fit improved error by %.2g percent'%(100*(1-initial_error[-1]/initial_error[0])))
if isinstance(temp, list) or isinstance(temp, type(np.arange(1))):
self.fitcoeff = temp
else:
raise
time = ds_back_corr.index.values.astype('float')#extract the time
self.ds = ds_back_corr.apply(lambda x:np.interp(x = time+np.polyval(temp, float(x.name)), xp = time, fp = x), axis = 0, raw = False)
fit_ds = sub_ds(ds = self.ds, scattercut = self.scattercut, bordercut = self.bordercut, timelimits = self.timelimits, wave_nm_bin = self.wave_nm_bin, equal_energy_bin = self.equal_energy_bin, time_bin = self.time_bin)
if pardf.vary.any():
results.params = results_in_chirp.params
return results, fit_ds
[docs] def Fit_Global(self, par = None, mod = None, confidence_level = None, use_ampgo = False, fit_chirp = False, fit_chirp_iterations = 10,
multi_project = None, unique_parameter = None, weights = None, same_DAS = False,
dump_paras = False, dump_shapes = False, filename = None, ext_spectra = None,
write_paras=False, tol = 1e-5, sub_sample=None,pulse_sample=None):
"""This function is performing a global fit of the data. As embedded object it uses
the parameter control options of the lmfit project as an essential tool.
(my thanks to Matthew Newville and colleagues for creating this phantastic tool)
[M. Newville, T. Stensitzki, D. B. Allen, A. Ingargiola, 2014. DOI: 10.5281/ZENODO.11813.].
The what type of fitting is performed is controlled by setting of the parameter here.
The general fitting follows this routine:
1. create a copy of the Data-Matrix self.ds is created with the shaping parameters
2. Then a Matrix is created that represents the fractional population of each species
(or processes in case of the paral model).
This Matrix contains one entry for each timepoint and represents the kinetic model
based upon the starting parameter. (see below for a description of the models).
This model formation can by done by using a build in or a user supplied function.
(handled in the function "pf.build_c")
-> If an ext_spectra is provided this its intensity is substacted from the matrix (only for external models)
3. Then the process/species associated spectra for each of the species is calculated
using the linalg.lstsq algorithm from numpy
(https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html)
4. From the convoluted calculated species concentrations and spectra a calculated matrix
is formed (handled in the function "pf.fill_int")
5. The difference between calculated and measured spectra is calculated, point-wise squared
and summed together. (function "err_func" or "err_func_multi" if multiple datasets are fitted)
6. This difference is minimized by iterating 2-4 with changing parameters using an
optimization algorithm (generally nelder-mead simplex)
7. Finally in a last run of 2-5 the final spectra are calculated (using the "final" flag)
and the optimized parameter, the matrixes
("A"-measured, "AC" - calculated, "AE" - linear error),
spectra (always called "DAS") the concentrations (called "c")
are written in the dictionary "ta.re" together with a few result representations
and other fit outputs. The optimized parameter are also written into ta.par_fit
(as an parameter object) that can be re-used as input into further optimization steps.
8. Under Windows we load the keyboard library and the Fit can be interrupted by pressing
the "q" key. Consider using the parameter write_paras or dump_paras to observe details during the fit.
All mandatory parameters are in general taken from the internal oject (self) The optional parameter control the behaviour of the fitting function
Parameters
------------------
par : lmfit parameter oject, optional
Here another parameter object could be given,overwriting the (Default is self.par)
mod : str or function, optional
Give a extra model selection (Default uses self.mod)
internal modells: 'paral','exponential','consecutive','full_consecutive'
see also :meth:`plot_func.build_c` and :meth:`plot_func.err_func`
confidence_level: None or float (0.5-1), optional
If this is changed from None (Default) to a value between 0.5 and 1 the code will
try to calculate the error of the parameter for the fit. For each parameter that
can vary a separate optimization is performed, that attempts to find the upper
and lower bound at which the total error of the re-optimized globally fitted results
reaches the by F-statistics defined confidence bound. See :meth:`plot_func.s2_vs_smin2` for details
on how this level is determined. Careful, this option might run for very long time.
Meaning that it typically takes 50 optimization per variable parameter (hard coded limit 200)
The confidence level is to be understood that it defines the e.g. 0.65 * 100\% area that the
parameter with this set of values is within this bounds.
Normal behaviour for this is to re-optimise the parameter during the optimization. if the parameter
par['error_param_fix'] is present, this will be suppressed.
use_ampgo : bool, optional
(Default) is False
Changes the optimizer from a pure Nelder mead to Ampgo with a local Nelder Mead.
For using this powerfull tool all parameter need to have a "min" and a "max" set.
Typically takes 10-40x longer than a standard optimization, but can due to its
tunneling algorithm more reliably find global minima.
see:https://lmfit.github.io/lmfit-py/fitting.html for further details
fit_chirp : bool, optional
(Default) is False
a powerful optimization of the chirp parameter. For this to work the data
needs to include timepoints before and after t=0 and one should have reached
a decent fit of most features in the spectrum. We perform an Nelder-Mead optimisation
of the parameter followed by a Nelder-Mead optimization of the chirp parameter
as one iteration. After each consecutive optimization it is checked if the total error
improved. If not the fit is ended, if yes the maximum number of iterations
'fit_chirp_iterations' is performed. Warning, this goes well in many cases,
but can lead to very strange results in others, always carefully check the results.
I recommend to make a copy of the object before runnning a chirp optimization.
fit_chirp_iterations : int, optional
maximum number of times the global - chirp loop is repeated.
Typically this iterations run 2-5 times, (Default) is 10
dump_paras : bool, optional
(Default) is False, If True creates two files in the working folder, one with the
currently used parameter created at the end of each optimisation step, and one with
the set of parameter that up to now gave the lowest error. Intented to store
the optimisation results if the fit needs to be interrupted
(if e.g. Ampgo simply needs to long to optimize.) useful option if things are slow
this parameter also triggers the writing of fitout to a textfile on disc
dump_shapes : bool, optional
this option dumps the concentratoin matrix and the DAS onto disk for each round of optimization,
mostly useful for multi-project fitting that wants to use the spectral or temporal intensity
write_paras : bool, optional
if True(Default) writes the currently varried values to screen
filename : None or str, optional
Only used in conjunction with 'dump_paras'. The program uses this filename to dump the
parameter to disk
multi_project : None or list (of TA projects), optional
This switch is triggering the simultaneous optimisation of multiple datasets.
multi_project is as (Default) None. it expects an iterable (typically list) with other
TA projects (like ta) that are then optimised with the same parameter.
This means that all projects get the same parameter object for each iteration
of the fit and return their individual error, which is summed linearly.
The "weights" option allows to give each multi_project a specific weight (number)
that is multiplied to the error. If the weight object has the same number of items
as the multi_project it is assumed that the triggering object (the embedded project)
has the weight of 1, otherwise the first weight is for the embedded project.
The option 'unique_parameter' takes (a list) of parameter that are not
to be shared between the projects (and that are not optimized either)
The intended use of this is to give e.g. the pump power for multiple experiments to
study non linear behaviour. Returned will be only the parameter set for the optimium
combination of all parameter. Internally, we iterate through the projects and calculate
for each project the error for each iteration. Important to note is that currently this
means that each DAS/SAS is calculated independently! For performing the same calculation
with a single DAS, the Matrixes need to be concatenated before the run and an external
function used to create a combined model. As this is very difficult to implement reliably
For general use (think e.g. different pump wavelength) this has to be done manually.
unique_parameter : None or str or list (of strings), optional
only used in conjunction with 'multi_project', it takes (a list) of parameter that
are not to be shared between the projects (and that are not optimized either)
The intended use of this is to give e.g. the pump power for multiple experiments
to study non linear behaviour. (Default) None
same_DAS : bool,optional
changes the fit behavior and uses the same DAS for the optimization.
This means that the ds are stacked before the fill_int rounds. This option is only used in multi-project fitting
weights : list of floats, optional
only used in conjunction with 'multi_project'. The "weights" option allows to
give each multi\_project a specific weight (number) that is multiplied to the error.
If the weight object has the same number of items as the 'multi_project' it is assumed
that ta (the embedded project) has the weight of 1, otherwise the first weight is for the
embedded object
ext_spectra : DataFrame, optional
(Default) is None, if given substract this spectra from the DataMatrix using the intensity
given in "C(t)" this function will only work for external models. The name of the spectral column
must be same as the name of the column used. If not the spectrum will be ignored. The spectrum will
be interpolated to the spectral points of the model ds before the substraction.
a number of parameters can be defined to aid this process. These parameter are defined as normal parameters.
"ext_spectra_scale" multiplies all spectra by this value (e.g. -1 to put the steady state absorption spectra in)
"ext_spectra_shift" shifts all spectra by this value to compensate for calibration differences
"ext_spectra_guide" (from version 7.1.0) This is a switch, if this keyword is present, then the spectra are
used as guides and not exclusively. This means the code will assume that these spectra are correct and substract
them, then calulate the difference and return as DAS the provided spectra plus the difference spectra
tol : float, optional
the tolerance value that is handed to the optimizer (absolute) for nelder-mead the moment this means:
df < tol (corresponds to fatol)
number_of_function_evaluations < maxfev (default 200 * n variables)
number_of_iterations < maxiter (default 200 * n variables)
Returns
------------------
re : dict
the dictionary "re" attached to the object containing all the matrixes and parameter.
The usual keys are:
"A" Shaped measured Matrix
"AC" Shaped calculated Matrix
"AE" Difference between A and AC = linear error
"DAS" DAS or SAS, labeled after the names given in the function (the columns of c) Care must be taken that this mesured intensity is C * DAS, the product. For exponential model the concentrations are normalized
"c" The Concentrations (meaning the evolution of the concentrations over time. Care must be taken that this mesured intensity is C * DAS, the product. For exponential model the concentrations are normalized
"fit_results_rates" DataFrame with the fitted rates (and the confidence intervals if calculated)
"fit_results_times" DataFrame with the fitted decay times (and the confidence intervals if calculated)
"fit_output" The Fit object as returned from lmfit. (This is not saved with the project!)
"error" is the S2, meaning AE**2.sum().sum()
"r2"=1-"error"/(('A'-'A'.mean())**2).sum(), so the residuals scaled with the signal size
par_fit : lmfit parameter object
is written into the object as a lmfit parameter object with the optimized results (that can be use further)
fitcoeff : list, if chirpfit is done
The chirp parameter are updated
ds : DataFrame, if chirpfit is done
A new ds is calculated form ds_ori if ChripFit is done
The rest is mainly printed on screen.
Examples
--------------------
Non optional:
>>> ta=pf.TA('testfile.SIA') #load data
>>> ta.mod='exponential' #define model
>>> ta.par=lmfit.Parameters() #create empty parameter object
>>> ta.par.add('k0',value=1/0.1,vary=True) #add at least one parameter to optimize
Trigger simple fit:
>>> ta.Fit_Global()
Trigger fit with Chrip Fit:
>>> ta.Fit_Global(fit_chirp=True)
Trigger iterative Chirp fitting with fresh refinement of the Global kinetic parametersfor i in range(5):
>>> for i in range(5):
>>> start_error=ta.re['error']
>>> ta.par=ta.par_fit
>>> ta.Fit_Global(fit_chirp=True)
>>> if not ta.re['error'] < start_error:break
Trigger fit fit error calculations
>>> ta.Fit_Global(confidence_level=0.66)
Trigger fit of multiple projects
#use the GUI_open function to open a list of objects (leave empty for using the GUI)
>>> other_projects=pf.GUI_open(['sample_1.hdf5','sample_2.hdf5'],path='Data')
>>> ta.Fit_Global(multi_project=other_projects)
For more examples please see the complete documentation under :ref:`Fitting, Parameter optimization and Error estimation`
or :ref:`Fitting multiple measured files at once`
"""
if par is None:par=self.par
if mod is None:mod=self.mod
try:
t0=par['t0']
except:
try:
par.add('t0',value=0,min=-0.5,max=0.5,vary=False)
except:
print("Unexpected error:", sys.exc_info()[0])
try:
resolution=par['resolution']
except:
try:
par.add('resolution',value=0.086,min=0.04,max=0.5,vary=False)
except:
print("Unexpected error:", sys.exc_info()[0])
try:
par['infinite'].value=1
par['infinite'].vary=False
except:
pass
try:
par['background'].value=1
par['background'].vary=False
except:
pass
try:
par['explicit_GS'].value=1
par['explicit_GS'].vary=False
except:
pass
try: # this is either freezing or enabling the re-optimization of all other parameter during confidence interval calculation
par['error_param_fix'].value=1
par['error_param_fix'].vary=False
vary_error_parameter=False
except:
vary_error_parameter=True
pardf=par_to_pardf(par)
pardf.loc[np.logical_and(pardf.loc[:,'min'].values<0,pardf.is_rate),'min']=0
pardf.loc[np.logical_and(pardf.loc[:,'max'].values<0,pardf.is_rate),'max']=0
pardf['init_value']=pardf['value']
if dump_paras:
pardf_temp=pardf.copy()
pardf_temp.loc['error','value']=1000
pardf_temp.to_csv('minimal_dump_paras.par')
if self.log_fit:
for key in ['value','min','max']:
pardf.loc[pardf.is_rate,key]=pardf.loc[pardf.is_rate,key].apply(lambda x: np.log10(x))
#create-shape the data to be fitted
fit_ds = sub_ds(ds = self.ds.copy(), scattercut = self.scattercut, bordercut = self.bordercut,
timelimits = self.timelimits, wave_nm_bin = self.wave_nm_bin, equal_energy_bin = self.equal_energy_bin,
time_bin = self.time_bin, ignore_time_region = self.ignore_time_region, drop_scatter = True, drop_ignore = True)
time_label=fit_ds.index.name
energy_label=fit_ds.columns.name
if pulse_sample is None:
if self.ignore_time_region is not None:
pulse_sample=True
if self.timelimits is not None:
if min(self.timelimits)>0:
pulse_sample=True
############################################################################
#----Global optimisation------------------------------------------------------
############################################################################
try:
keyboard.__package__
def iter_cb(params, iterative, resid, ds=None,mod=None,log_fit=None,final=None,dump_paras=None,filename=None,ext_spectra=None,dump_shapes=None,
write_paras=None,multi_project=None,unique_parameter=None,
weights=None,same_DAS=None,sub_sample=None,pulse_sample=None):
if keyboard.is_pressed("q"):
print('---------------------------------------------')
print('--------- Interupted by user ------')
print('---------------------------------------------')
print('----------- Last fitted parameter ------')
print(par_to_pardf(params))
return True
else:
return None
except:
def iter_cb(params, iterative, resid, ds=None,mod=None,log_fit=None,final=None,dump_paras=None,filename=None,ext_spectra=None,dump_shapes=None,
write_paras=None,multi_project=None,unique_parameter=None,
weights=None,same_DAS=None,sub_sample=None,pulse_sample=None):
return None
if multi_project is None:
#check if there is any concentration to optimise
if (filename is None) and dump_shapes: filename = self.filename
if pardf.vary.any():#ok we have something to optimize
mini = lmfit.Minimizer(err_func,pardf_to_par(pardf),iter_cb=iter_cb,
fcn_kws={'ds':fit_ds,'mod':mod,'log_fit':self.log_fit,'final':False,
'dump_paras':dump_paras,'filename':filename,'ext_spectra':ext_spectra,
'dump_shapes':dump_shapes, 'write_paras':write_paras,'sub_sample':sub_sample,'pulse_sample':pulse_sample})
if not use_ampgo:
if len(pardf[pardf.vary].index)>3:
print('we use adaptive mode for nelder')
#results = mini.minimize('nelder',options={'adaptive':True,'fatol':tol})
results = mini.minimize('nelder',tol=tol,options={'adaptive':True})
else:
results = mini.minimize('nelder',tol=tol)
#results = mini.minimize('nelder',options={'fatol':tol})
else:
results = mini.minimize('ampgo',**{'local':'Nelder-Mead'})
############################################################################
#----Multi project Global optimisation----------------------------------------
##########################################################################
else:
fit_chirp=False #chirp fitting currently only works for single problems
if pardf.vary.any():#ok we have something to optimize lets return the spectra
multi_project.insert(0,self)
mini = lmfit.Minimizer(err_func_multi,pardf_to_par(pardf),iter_cb=iter_cb,
fcn_kws={'multi_project':multi_project,'unique_parameter':unique_parameter,
'weights':weights,'mod':mod,'log_fit':self.log_fit,'final':False,
'dump_paras':dump_paras,'filename':filename,'ext_spectra':ext_spectra,
'dump_shapes':dump_shapes,'same_DAS':same_DAS,'sub_sample':sub_sample,'pulse_sample':pulse_sample})
if len(pardf[pardf.vary].index)>3:
print('we use adaptive mode for nelder')
results = mini.minimize('nelder',options={'adaptive':True,'fatol':tol})
else:
results = mini.minimize('nelder',options={'fatol':tol})
#######################################################################
#----Fit chirp----------------------------------------------------------------------------------
####################################################################
if self.ignore_time_region is not None:
if fit_chirp:
print('sorry but currently you can not both ignore a time region and fit the chirp (assuming that you ignore the time-zero region)')
fit_chirp=False
if fit_chirp:
print('Done initial fitting now chirpfit')
results,fit_ds=self.__Fit_Chirp_outer(pardf,results,fit_ds,fit_chirp_iterations,mod)
####################################################################
#------Write results to parameter------------------------
############################################################
if pardf.vary.any():#we actually have optimised something
pardf['value']=par_to_pardf(results.params)['value']
if self.log_fit:
for key in ['value','min','max']:
pardf.loc[pardf.is_rate,key]=pardf.loc[pardf.is_rate,key].apply(lambda x: 10**x)
self.par_fit=pardf_to_par(pardf)
else:
print('ATTENTION: we have not optimized anything but just returned the parameters')
self.par_fit=self.par
if multi_project is None:
re=err_func(paras=self.par_fit,ds=fit_ds,mod=self.mod,final=True,log_fit=self.log_fit,ext_spectra=ext_spectra,sub_sample=sub_sample,pulse_sample=pulse_sample)
else:
if same_DAS:
re_listen = err_func_multi(paras = self.par_fit, mod = mod, final = True, log_fit = self.log_fit,
multi_project = multi_project, unique_parameter = unique_parameter, same_DAS = same_DAS, weights = weights,
ext_spectra = ext_spectra,sub_sample=sub_sample,pulse_sample=pulse_sample)
re=re_listen[0]
else:
re = err_func_multi(paras = self.par_fit, mod = mod, final = True, log_fit = self.log_fit,
multi_project = multi_project, unique_parameter = unique_parameter, same_DAS = same_DAS, weights = weights,
ext_spectra = ext_spectra,sub_sample=sub_sample,pulse_sample=pulse_sample)
############################################################################
#----Estimate errors---------------------------------------------------------------------
############################################################################
if confidence_level is not None:#ok we calculate errors to the level of the confidence_level
if self.log_fit:
for key in ['value','min','max']:
pardf.loc[pardf.is_rate,key]=pardf.loc[pardf.is_rate,key].apply(lambda x: np.log10(x))
if pardf.vary.any():#we actually have optimised something
if (0.6 < confidence_level < 1) or (1 < confidence_level < 0.6):
if multi_project is None:
target=s2_vs_smin2(Spectral_points=len(re['A'].columns),Time_points=len(re['A'].index),number_of_species=len(re['DAC'].columns),fitted_kinetic_pars=len(pardf[pardf.vary].index),target_quality=confidence_level)
else:
multi_project.insert(0,self)
# we assume that we have the same number of spectal points but are stacking the times
total_time_points=np.array([len(t.re['A'].index) for t in multi_project]).sum()
target=s2_vs_smin2(Spectral_points=len(re['A'].columns),Time_points=total_time_points,number_of_species=len(re['DAC'].columns),fitted_kinetic_pars=len(pardf[pardf.vary].index),target_quality=confidence_level)
#print(target)
target_s2=re['error']*target
list_of_variable_parameter=pardf[pardf.vary].index.values
conf_limits={}
iterative_calls=0
for fixed_par in list_of_variable_parameter:
conf_limits[fixed_par]={'upper':None,'lower':None}
for i in ['lower','upper']:
print('Trying to find %s, %s confidence limit'%(fixed_par,i))
pardf_local=self.par_fit.copy()
pardf_local[fixed_par].vary=False
par_local=lmfit.Parameters()
if 'lower' in i:#go below min
if par_to_pardf(pardf_local).loc[fixed_par,'is_rate']:
par_local.add(fixed_par,value=pardf_local[fixed_par].value*0.95,min=0,max=pardf_local[fixed_par].value,vary=vary_error_parameter)
else:
par_local.add(fixed_par,value=pardf_local[fixed_par].value*0.95,max=pardf_local[fixed_par].value,vary=vary_error_parameter)
else: #go above min
par_local.add(fixed_par,value=pardf_local[fixed_par].value*1.05,min=pardf_local[fixed_par].value,vary=vary_error_parameter)
def sub_problem(par_local,varied_par,pardf_local,fit_ds=None,mod=None,log_fit=None,multi_project=None,unique_parameter=None,weights=None,target_s2=None,ext_spectra=None,same_DAS=False ):
pardf_local[varied_par].value=par_local[varied_par].value
if par_to_pardf(pardf_local).vary.any():
if multi_project is None:
mini_sub = lmfit.Minimizer(err_func,pardf_local,fcn_kws={'ds':fit_ds,'mod':mod,'log_fit':log_fit,'ext_spectra':ext_spectra})
else:
mini_sub = lmfit.Minimizer(err_func_multi,pardf_local,fcn_kws={'multi_project':multi_project,'unique_parameter':unique_parameter,'weights':weights,
'same_DAS':same_DAS,'mod':mod,'log_fit':log_fit,'ext_spectra':ext_spectra,'sub_sample':sub_sample,'pulse_sample':pulse_sample})
if len(pardf[pardf.vary].index)>3:
results_sub = mini_sub.minimize('Nelder',options={'xatol':0.01,'adaptive':True})
else:
results_sub = mini_sub.minimize('Nelder',options={'xatol':0.01})
local_error=(results_sub.residual[0]-target_s2)**2
return local_error
else:
if multi_project is None:
return err_func(pardf_local,ds=fit_ds,mod=mod,log_fit=log_fit,ext_spectra=ext_spectra)
else:
return err_func_multi(pardf_local,multi_project=multi_project,unique_parameter=unique_parameter,weights=weights,mod=mod,log_fit=log_fit,ext_spectra=ext_spectra)
try:
mini_local = lmfit.Minimizer(sub_problem,par_local,fcn_kws={'varied_par':fixed_par,'pardf_local':pardf_local,'fit_ds':fit_ds,
'multi_project':multi_project, 'unique_parameter':unique_parameter,'same_DAS':same_DAS,'weights':weights,
'mod':mod,'log_fit':self.log_fit,'target_s2':target_s2,'ext_spectra':ext_spectra,'sub_sample':sub_sample,'pulse_sample':pulse_sample})
one_percent_precission=(target-1)*0.01*re['error']
#results_local = mini_local.minimize('least_squares',ftol=one_percent_precission)
results_local = mini_local.minimize(method='nelder',options={'maxiter':100,'fatol':one_percent_precission})
iterative_calls+=results_local.nfev
if results_local.success:
conf_limits[fixed_par][i]=results_local.params[fixed_par].value
else:
print("tried to optimise %i times achieved residual %g with targeted %g"%(results_local.nfev,(np.sqrt(results_local.residual[0])+target_s2),target_s2))
except:
#print("Unexpected error:", sys.exc_info()[0])
print("error in %s at %s limit"%(fixed_par,i))
continue
else:
print("please use a confidence level between 0.6 and 1")
return False
print("it took %i optimisations to get the confidence"%iterative_calls)
############################################################################
#-----prepare frames for storage without confidence and store them------------------------
############################################################################
if pardf.vary.any():
re['fit_output']=results#let's store the fit results in the re_object for now.
if confidence_level is not None:
re['confidence']=conf_limits
pardf.insert(len(pardf.columns),'lower_limit',None)
pardf.insert(len(pardf.columns),'upper_limit',None)
for key in conf_limits.keys():
pardf.loc[key,'lower_limit']=conf_limits[key]['lower']
pardf.loc[key,'upper_limit']=conf_limits[key]['upper']
if self.log_fit:
for key in ['value','min','max','lower_limit','upper_limit']:
for row in pardf[pardf.is_rate].index.values:
try:
pardf.loc[row,key]=10**pardf.loc[row,key]
except:
if pardf.loc[row,key] is None:
continue
elif pardf.loc[row,key].isnan():
continue
else:
print('%s,%s has could not be converted and has value'%(row,key))
print(pardf.loc[row,key])
continue
re['confidence']['target-level']='%.1f\n'%((confidence_level)*100)
re['fit_results_rates']=pardf
timedf=pardf_to_timedf(pardf)
re['fit_results_times']=timedf
if same_DAS:
for i,re_local in enumerate(re_listen):
for name in ['fit_output','fit_results_rates','fit_results_times']:
try:
re_listen[i][name]=re[name]
except:
print(name + 'not found')
###############################################
##convert energy back to wavelength#############
################################################
if 1:
if self.equal_energy_bin is not None:
if same_DAS:
for i,re_local in enumerate(re_listen):
for name in ['A','AC','AE']:
re_local[name].columns=(scipy.constants.h*scipy.constants.c/(re_local[name].columns.values*1e-9*scipy.constants.electron_volt))
re_local[name].columns.name='wavelength in nm'
re_local[name].sort_index(inplace=True,axis=1,ascending=True)
re_local['DAC'].index=(scipy.constants.h*scipy.constants.c/(re_local['DAC'].index.values*1e-9*scipy.constants.electron_volt))
re_local['DAC'].index.name='wavelength in nm'
re_local['DAC'].sort_index(inplace=True,axis=0,ascending=True)
re_listen[i]=re_local
else:
for name in ['A','AC','AE']:
re[name].columns=(scipy.constants.h*scipy.constants.c/(re[name].columns.values*1e-9*scipy.constants.electron_volt))
re[name].columns.name='wavelength in nm'
re[name].sort_index(inplace=True,axis=1,ascending=True)
re['DAC'].index=(scipy.constants.h*scipy.constants.c/(re['DAC'].index.values*1e-9*scipy.constants.electron_volt))
re['DAC'].index.name='wavelength in nm'
re['DAC'].sort_index(inplace=True,axis=0,ascending=True)
############################################################################
#---print the output---------------------------------------------------
############################################################################
self.re=re
if same_DAS:
re_listen[0]=re
self.multi_projects=re_listen
Result_string='\nFit Results:\n'
if isinstance(mod,type('hello')):
Result_string+='Model Used: %s\n\n'%mod
else:
Result_string+='Model Used: External function\n\n'
if self.ignore_time_region is not None:
try:
Result_string+='the time between %.3f %s and %.3f %s was excluded from the optimization\n\n'%(self.ignore_time_region[0],self.baseunit,self.ignore_time_region[1],self.baseunit)
except:#we got a list
for entry in self.ignore_time_region:
Result_string+='the time between %.3f %s and %.3f %s was excluded from the optimization\n\n'%(entry[0],self.baseunit,entry[1],self.baseunit)
Result_string+='The minimum error is:{:.8e}\n'.format(re['error'])
try:
Result_string+='The minimum R2-value is:{:.8e}\n'.format(re['r2'])
except:
pass
if same_DAS:
Result_string+='The minimum global error is:{:.8e}\n'.format(re['error_total'])
Result_string+='The minimum global R2-value is:{:.8e}\n'.format(re['r2_total'])
if confidence_level is not None:
Result_string+='\nIn Rates with confidence interval to level of %.1f\n\n'%((confidence_level)*100)
Result_string+=pardf.loc[:,['value','lower_limit','upper_limit','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
Result_string+='\n\nThe rates converted to times with unit %s with confidence interval to level of %.1f\n\n'%(self.baseunit,(confidence_level)*100)
Result_string+=timedf.loc[:,['value','lower_limit','upper_limit','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
else:
Result_string+='\nIn Rates\n\n'
Result_string+=pardf.loc[:,['value','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
Result_string+='\n\nThe rates converted to times with unit %s\n\n'%self.baseunit
Result_string+=timedf.loc[:,['value','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
if same_DAS:
Result_string+='\n\nthe other objects were layed into self.multi_projects as list with the local re on position 0.\n By replacing assuming that self = ta write: \n ta.re = ta.multi_projects[1] and then ta.Plot_fit_output to look on the other fits\n '
try:
if not results.aborted:
print(Result_string)
except:
print(Result_string)
if same_DAS:
for i,re_local in enumerate(re_listen):
re_listen[i]['Result_string']=Result_string
else:
re['Result_string']=Result_string
if dump_paras:
with open("Fit_results_print.par", "w") as text_file:
text_file.write(Result_string)
[docs] def Plot_fit_output(self, plotting = range(6), path = 'result_figures', savetype = 'png',
evaluation_style = False, title = None, scale_type = 'symlog',
patches = False, filename = None, cmap = None , print_click_position = False,
plot_second_as_energy = True):
'''plots all the fit output figures. The figures can be called separately
or with a list of plots. e.g. range(6) call plots 0-5 Manual plotting of certain type:
This is a wrapper function that triggers the plotting of all the fitted plots.
The parameter in this plot call are to control the general look and features of the plot.
Which plots are printed is defined by the first command (plotting)
The plots are generated from the fitted Matrixes and as such only will work after a fit was actually
completed (and the "re" dictionary attached to the object.)
In all plots the RAW data is plotted as dots and the fit with lines
Contents of the plots
0. DAC contains the assigned spectra for each component of the fit. For
a modelling with independent exponential decays this corresponds to
the "Decay Associated Spectra" (DAS). For all other models this
contains the "Species Associated Spectra" (SAS). According to the
model the separate spectra are labeled by time (process) or name, if
a name is associated in the fitting model. The spectra are shown in
the extracted strength in the right pane and normalized in the left.
Extracted strength means that the measured spectral strength is the
intensity (concentration matrix) times this spectral strength. As the
concentration maxima for all DAS are 1 this corresponds to the
spectral strength for the DAS. (please see the documentation for the
fitting algorithm for further details).
1. summed intensity. All wavelength of the spectral axis are summed for
data and fit. The data is plotted in a number of ways vs linear and
logarithmic axis. This plot is not ment for publication but very
useful to evaluate the quality of a fit.
2. plot kinetics for selected wavelength (see corresponding RAW plot).
3. plot spectra at selected times (see corresponding RAW plot).
4. plots matrix (measured, modelled and error Matrix). The parameter are
the same as used for the corresponding RAW plot with the addition of
"error_matrix_amplification" which is a scaling factor multiplied
onto the error matrix. I recommend to play with different "cmap",
"log_scale" and "intensity_scale" to create a pleasing plot.
5. concentrations. In the progress of the modelling/fitting a matrix is
generated that contains the relative concentrations of the species
modelled. This plot is showing the temporal development of these
species. Further details on how this matrix is generated can be found
in the documentation of the fitting function. The modeled spectra are
the convolution of these vectors (giving the time-development) and
the DAS/SAS (giving the spectral development).
Parameters
---------------
plotting : int or iterable (of integers), optional
This parameter determines which figures are plotted
the figures can be called separately with plotting = 1
or with a list of plots (Default) e.g. plotting=range(6) calls plots 0,1,2,3,4,5
The plots have the following numbers:
0. DAS or SAS
1. summed intensity
2. Kinetics
3. Spectra
4. Matrixes
5. Concentrations (the c-object)
The plotting takes all parameter from the "ta" object unless otherwise specified
path : None, str or path object, optional
This defines where the files are saved if the safe_figures_to_folder parameter is True,
quite useful if a lot of data sets are to be printed fast.
If a path is given, this is used. If a string like the (Default) "result_figures" is given,
then a subfolder of this name will be used (an generated if necessary)
relative to self.path. Use and empty string to use the self.path
If set to None, the location of the plot_func will be used and
a subfolder with title "result_figures" be generated here
savetype : str or iterable (of str), optional
matplotlib allows the saving of figures in various formats. (Default) "png",
typical and recommendable options are "svg" and "pdf".
evaluation_style : bool, optional
True (Default = False) adds a lot of extra information in the plot
title : None or str, optional
"title=None" is in general the filename that was loaded. Setting a
specific title will be used in all plots. To remove the title all
together set an empty string with title=""
scale_type : str, optional
refers to the time-axis and takes, "symlog" (Default)(linear around zero and logarithmic otherwise)
and "lin" for linear and "log" for logarithmic, switching all the time axis to this type
patches : bool, optional
If False (Default) the names "measured" "fitted" "difference" will be placed above the images.
If True, then they will be included into the image (denser)
filename : str, optional
offers to replace the base-name used for all plots (to e.g.specify what sample was used).
if (Default) None is used, the self.filename is used as a base name. The filename plays only a
role during saving, as does the path and savetype
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
print_click_position : bool, optional
if True then the click position is printed for the spectral plots
Examples
------------
Typically one would call this function empty for an overview:
After the minimum fit
>>> ta=pf.TA('testfile.SIA')
>>> ta.par=lmfit.Parameters()
>>> ta.par.add('k0',value=1/0.1,vary=True)
>>> ta.Fit_Global()
One usually plots the an overview
>>> ta.Plot_fit_output()
>>> ta.Plot_fit_output(plotting=range(6)) #is the same as before
>>> ta.Plot_fit_output(2) #would plot only the kinetics
>>> ta.Plot_fit_output(plotting = 2) #would plot only the kinetics
'''
try:
re=self.re
except:
print('We need to have fitted something so that we can plot')
return False
path=check_folder(path=path,current_path=self.path)
if self.save_figures_to_folder:
self.figure_path=path
if cmap is None:cmap=self.cmap
if filename is None:filename=self.filename
if title is None:
if filename is None:
title=self.filename
else:
title=filename
if not hasattr(plotting,"__iter__"):plotting=[plotting]
plot_fit_output(self.re, self.ds, cmap = cmap, plotting = plotting, title = title,
path = path, f = filename, intensity_range = self.intensity_range,
log_scale = self.log_scale, baseunit = self.baseunit, timelimits = self.timelimits,
scattercut = self.scattercut, bordercut = self.bordercut,
error_matrix_amplification = self.error_matrix_amplification,
wave_nm_bin = self.wave_nm_bin, rel_wave = self.rel_wave, width = self.wavelength_bin,
rel_time = self.rel_time, save_figures_to_folder = self.save_figures_to_folder,
log_fit = self.log_fit,mod = self.mod, savetype = savetype,
time_width_percent = self.time_width_percent, evaluation_style = evaluation_style,
filename = self.filename, scale_type = scale_type, patches = patches, lintresh = self.lintresh,
print_click_position = print_click_position, ignore_time_region = self.ignore_time_region,
data_type = self.data_type, plot_second_as_energy = plot_second_as_energy, units= self.units,
equal_energy_bin = self.equal_energy_bin)
[docs] def Save_data(self, save_RAW = True, save_Fit = True, save_slices = True, save_binned = False,
filename = None, save_fit_results = True, path = 'Data_export', sep = str('\t')):
'''handy function to save the data on disk as dat files.
The RAW labeled files contain the chirp corrected values (self.ds)
the save_slices switch turns on the dump of the separate sliced figures (time and spectral)
Parameters
----------
save_binned : bool, optional
is also the re-binned matrix to be saved.
save_slices : bool, optional
save the kinetics and spectra from the fitted data (with the fits)
sep : str, optional
what symbol is used to separate different number. (typical either 'tab' or comma
save_RAW : bool, optional
(Default) True then the first slide with the RAW data is created
save_Fit : bool, optional
(Default) True then the second slide with the Fitted data is created
path : None, str or path, optional
(Default) None, if left on None, then a folder "result_figures" is created in the folder
of the data (self.path)
save_fit_results : bool, optional
if True (Default) a neatly formated file with the fit results is created and stored with the data
filename : str, optional
(Default) None, Base name for all plots. If None, then self.filename will be used
Examples
---------
>>> ta.Save_Data
'''
if filename is None:filename = self.filename.split('.')[0]
if save_RAW:
self.ds.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_chirp_corrected_raw_matrix.dat'), sep = sep)
if save_binned:
sub = sub_ds(self.ds, scattercut = self.scattercut, bordercut = self.bordercut,
timelimits = self.timelimits, wave_nm_bin = self.wave_nm_bin,
time_bin = self.time_bin)
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_chirp_corrected_rebinned_matrix.dat'), sep = sep)
if save_slices:
sub = sub_ds(ds = self.ds.copy(), wavelength_bin = self.wavelength_bin, wavelength = self.rel_wave)
#sub.columns.name = 'wavelength [nm] in %.0f bins'%self.wavelength_bin
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_chirp_corrected_RAW_kinetics.dat'), sep = sep)
sub = sub_ds(ds = self.ds.copy(), times = self.rel_time, time_width_percent = self.time_width_percent,
scattercut = self.scattercut, bordercut = self.bordercut, wave_nm_bin = self.wave_nm_bin)
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_chirp_corrected_RAW_Spectra.dat'), sep = sep)
if save_Fit:
try:
self.re.keys()
except:
print('no fit in data')
save_Fit = False
if save_Fit:
self.re['A'].to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_matrix used as fit input.dat'), sep = sep)
self.re['AC'].to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_matrix calculated during fit.dat'), sep = sep)
self.re['AE'].to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_error_matrix calculated during fit.dat'), sep = sep)
if save_slices:
sub = sub_ds(ds = self.re['AC'].copy(), wavelength_bin = self.wavelength_bin, wavelength = self.rel_wave)
#sub.columns.name = 'wavelenth [nm] in %.0f bins'%self.wavelength_bin
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_fitted_kinetics.dat'), sep = sep)
sub = sub_ds(ds = self.re['A'].copy(), wavelength_bin = self.wavelength_bin, wavelength = self.rel_wave)
#sub.columns.name = 'wavelenth [nm] in %.0f bins'%self.wavelength_bin
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_measured_kinetics.dat'), sep = sep)
sub = sub_ds(ds = self.re['AC'].copy(), times = self.rel_time,
time_width_percent = self.time_width_percent, scattercut = self.scattercut,
bordercut = self.bordercut, wave_nm_bin = self.wave_nm_bin)
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_fitted_spectra.dat'), sep = sep)
sub = sub_ds(ds = self.re['A'].copy(), times = self.rel_time,
time_width_percent = self.time_width_percent, scattercut = self.scattercut,
bordercut = self.bordercut, wave_nm_bin = self.wave_nm_bin)
sub.to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_measured_spectra.dat'), sep = sep)
self.re['DAC'].to_csv(check_folder(path = path, current_path = self.path,
filename = filename+'_DAS-SAS.dat'), sep = sep)
if save_fit_results:
Result_string='\nFit Results:\n'
if isinstance(self.mod,type('hello')):
Result_string+='Model Used: %s\n\n'%self.mod
else:
Result_string+='Model Used: External function\n\n'
if self.ignore_time_region is not None:
Result_string+='the time between %.3f %s and %.3f %s was excluded from the optimization\n'%(self.ignore_time_region[0],self.baseunit,self.ignore_time_region[1],self.baseunit)
Result_string+='The minimum error is:{:.8e}\n'.format(self.re['error'])
Result_string+='The minimum R2-value is:{:.8e}\n'.format(self.re['r2'])
if 'confidence' in self.re:
Result_string+='\nIn Rates with confidence interval to level of %s\n'%self.re['confidence']['target-level']
Result_string+=self.re['fit_results_rates'].to_string(columns=['value','lower_limit','upper_limit','init_value','vary','min','max','expr'])
Result_string+='\n\nThe rates converted to times with unit %s with confidence interval to level of %s\n'%(self.baseunit,self.re['confidence']['target-level'])
Result_string+=self.re['fit_results_times'].to_string(columns=['value','lower_limit','upper_limit','init_value','vary','min','max','expr'])
else:
Result_string+='\nIn Rates\n'
Result_string+=self.re['fit_results_rates'].to_string(columns=['value','init_value','vary','min','max','expr'])
Result_string+='\n\nThe rates converted to times with unit %s\n'%self.baseunit
Result_string+=self.re['fit_results_times'].to_string(columns=['value','init_value','vary','min','max','expr'])
with open(check_folder(path = path, current_path = self.path, filename = filename+'_fit_results_parameter.par'), "w") as text_file:
text_file.write(Result_string)
[docs] def Save_Powerpoint(self, save_RAW = True, save_Fit = True, filename = None,
path = 'result_figures', scale_type = 'symlog', title = None, patches = False, cmap=None , savetype = 'pptx'):
'''This function creates two power point slides. On the first it summarizes the RAW plots and on
the second (if existent) it summarizes the fitted results
Parameters
----------
save_RAW : bool, optional
(Default) True then the first slide with the RAW data is created
save_Fit : bool, optional
(Default) True then the second slide with the Fitted data is created
path : None, str or path, optional
(Default) None, if left on None, then a folder "result_figures" is created in the folder
of the data (self.path)
savetype : str or iterable (of str), optional
triggers the additional creation of a composite file in this format.
matplotlib allows the saving of figures in various formats. (Default) "png",
typical and recommendable options are "svg" and "pdf".
title : None or str, optional
(Default) None, Use this title on all plots. if None, use self.filename
filename : str, optional
(Default) None, Base name for all plots. If None, then self.filename will be used
scale_type : str, optional
'symlog' (Default), 'linear', 'log' time axis
patches : bool, optional
For true use white patches to label things in the 2d matrixes, to safe
space for publication
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
Examples
---------
>>> ta.Save_Powerpoint()
>>> ta.Save_Powerpoint(patches = True)
'''
if isinstance(savetype,type('hello')):savetype=[savetype]
if not hasattr(savetype,"__iter__"):savetype=[savetype]
raw_names=["MAT","SEL","SPEK","SVD"]
raw_names=[check_folder(current_path=self.path, path=path, filename=self.filename.split('.')[0] + "_RAW_"+str(a) +".png") for a in raw_names]
fit_names=["FIG_MAT","SPEC","SEL","SUM","DAC"]
fit_names=[check_folder(current_path=self.path, path=path, filename=self.filename.split('.')[0] + "_" +str(a) +".png") for a in fit_names]
plt.close('all')
origin=self.save_figures_to_folder
if filename is None:
filename=self.filename
filename=filename.split('.')[0]
if save_RAW:
self.save_figures_to_folder=True
self.Plot_RAW(savetype = 'png', scale_type = scale_type, title = title, cmap = cmap, path = path)
plt.close('all')
if save_Fit:
try:
self.save_figures_to_folder=True
self.Plot_fit_output(savetype = 'png', scale_type = scale_type, title = title, patches = patches, cmap = cmap , path = path)
plt.close('all')
except:
save_Fit = False
print('run into problems with adding the fit results. Have you fitted something?')
try:
if 'Result_string' in self.re:
Result_string=self.re['Result_string']
else:# to allow the use of old saved results. Will be deprecated in a few versions
Result_string='\nFit Results:\n'
if isinstance(mod,type('hello')):
Result_string+='Model Used: %s\n\n'%mod
else:
Result_string+='Model Used: External function\n\n'
if self.ignore_time_region is not None:
try:
Result_string+='the time between %.3f %s and %.3f %s was excluded from the optimization\n\n'%(self.ignore_time_region[0],self.baseunit,self.ignore_time_region[1],self.baseunit)
except:#we got a list
for entry in self.ignore_time_region:
Result_string+='the time between %.3f %s and %.3f %s was excluded from the optimization\n\n'%(entry[0],self.baseunit,entry[1],self.baseunit)
Result_string+='The minimum error is:{:.8e}\n'.format(re['error'])
try:
Result_string+='The minimum R2-value is:{:.8e}\n'.format(re['r2'])
except:
pass
if same_DAS:
Result_string+='The minimum global error is:{:.8e}\n'.format(re['error_total'])
Result_string+='The minimum global R2-value is:{:.8e}\n'.format(re['r2_total'])
if confidence_level is not None:
Result_string+='\nIn Rates with confidence interval to level of %.1f\n\n'%((confidence_level)*100)
Result_string+=pardf.loc[:,['value','lower_limit','upper_limit','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
Result_string+='\n\nThe rates converted to times with unit %s with confidence interval to level of %.1f\n\n'%(self.baseunit,(confidence_level)*100)
Result_string+=timedf.loc[:,['value','lower_limit','upper_limit','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
else:
Result_string+='\nIn Rates\n\n'
Result_string+=pardf.loc[:,['value','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
Result_string+='\n\nThe rates converted to times with unit %s\n\n'%self.baseunit
Result_string+=timedf.loc[:,['value','init_value','vary','min','max','expr']].to_markdown(tablefmt="grid")
if same_DAS:
Result_string+='\n\nthe other objects were layed into self.multi_projects as list with the local re on position 0.\n By replacing assuming that self = ta write: \n ta.re = ta.multi_projects[1] and then ta.Plot_fit_output to look on the other fits\n '
Result_string=Result_string.replace('lower_limit','low_lim')
Result_string=Result_string.replace('upper_limit','up_lim')
Result_string.replace('===','=')
except Exception as e:
print(e)
if ('pptx' in savetype) or ('ppt' in savetype):
try:
left=Inches(0.2)
top=Inches(0.2)
prs = Presentation()
blank_slide_layout = prs.slide_layouts[6]
if save_RAW:
slide = prs.slides.add_slide(blank_slide_layout)
left = top = Inches(0.5)
pic = slide.shapes.add_picture(str(raw_names[0].resolve()), left=left+Inches(4.5), top=top, width=Inches(4.5))
pic = slide.shapes.add_picture(str(raw_names[1].resolve()), left=left, top=top, width=Inches(4.5))
pic = slide.shapes.add_picture(str(raw_names[2].resolve()), left=left, top=top+Inches(3), width=Inches(4.5))
try:
pic = slide.shapes.add_picture(str(raw_names[3].resolve()), left=left+Inches(4.5), top=top+Inches(3), height=Inches(3.4))
except:
pass
if save_Fit:
try:
slide2 = prs.slides.add_slide(blank_slide_layout)
left = top = Inches(0.1)
pic = slide2.shapes.add_picture(str(fit_names[0].resolve()), left=left+Inches(7.0), top=top, height=Inches(3.4))#Matrix
pic = slide2.shapes.add_picture(str(fit_names[1].resolve()), left=left, top=top, height=Inches(2))
pic = slide2.shapes.add_picture(str(fit_names[2].resolve()), left=left, top=top+Inches(2), height=Inches(2))
pic = slide2.shapes.add_picture(str(fit_names[3].resolve()), left=left, top=top+Inches(3.9), height=Inches(1.4))
pic = slide2.shapes.add_picture(str(fit_names[4].resolve()), left=left, top=top+Inches(5.4), height=Inches(2))
text1 = slide2.shapes.add_textbox(left=left+Inches(5.2), top=top+Inches(2.5), width=Inches(4.5), height=Inches(4.5))
text1.text = '{}'.format(Result_string.replace('===','='))
try:
text1.text_frame.fit_text(font_family='Garamond', max_size=6, bold=True, italic=False)
#text1.text_frame.fit_text(font_family='Haettenschweiler', max_size=6, bold=False, italic=False)
except:
text1.text_frame.fit_text(font_family='Arial', max_size=5.0, bold=False, italic=False)
except Exception as e:
print('exited when saving the fit plots')
print(e)
plt.close('all')
self.save_figures_to_folder=origin
prs.save(check_folder(path=path,current_path=self.path,filename=self.filename.split('.')[0] + '.pptx'))
print('The images and a powerpoint was saved to %s'%check_folder(path=path,current_path=self.path))
except Exception as e:
print('Error in powerpoint generation. Most likely a module is missing.')
print('We need python-pptx to create a powerpoint file. Either use "pip install python-pptx" or "conda install -c conda-forge python-pptx" ')
print('We will save the results as pdf format for now. Check th error if somehting else went wrong')
print(e)
savetype.append('pdf')
if ('pdf' in savetype) or ('png' in savetype) or ('svg' in savetype):
if save_RAW:
fig,ax=plt.subplots(nrows=2,ncols=2,figsize=(10,7.5))
ax[0,1].imshow(mpimg.imread(str(raw_names[0])))
ax[0,0].imshow(mpimg.imread(str(raw_names[1])))
ax[1,0].imshow(mpimg.imread(str(raw_names[2])))
ax[1,1].imshow(mpimg.imread(str(raw_names[3])))
ax[0,0].axis('off');ax[1,0].axis('off');ax[0,1].axis('off');ax[1,1].axis('off')
for entry in savetype:
if entry == "pptx":continue
try:
fig.tight_layout()
fig.savefig(check_folder(path=path,current_path=self.path,filename=self.filename.split('.')[0] + '_RAW-summary.%s'%entry),dpi=600)
except:
print("saving in" + entry +"failed")
if save_Fit:
G = GridSpec(4, 8)
fig1=plt.figure(figsize=(8,10))
ax1=fig1.add_subplot(G[0,:5])
ax2=fig1.add_subplot(G[1,:5])
ax3=fig1.add_subplot(G[2,:6])
ax4=fig1.add_subplot(G[3,:6])
ax5=fig1.add_subplot(G[0:2,5:])
ax6=fig1.add_subplot(G[1:,6:])
ax1.imshow(mpimg.imread(str(fit_names[1])))
ax2.imshow(mpimg.imread(str(fit_names[2])))
ax3.imshow(mpimg.imread(str(fit_names[3])))
ax4.imshow(mpimg.imread(str(fit_names[4])))
ax5.imshow(mpimg.imread(str(fit_names[0])))
ax6.text(0,0,Result_string.replace('===','='),font='Garamond',fontsize=6,fontweight='bold')
ax1.axis('off');ax2.axis('off');ax3.axis('off');ax4.axis('off');ax5.axis('off');ax6.axis('off')
for entry in savetype:
if entry == "pptx": continue
try:
fig1.tight_layout()
fig1.savefig(check_folder(path=path,current_path=self.path,filename=self.filename.split('.')[0] + '_Fit-summary.%s'%entry),dpi=600)
except:
print("saving in" + entry +"failed")
def Print_Results(self,to_file=False):
if 're' in self.__dict__:
try:
print('{}'.format(self.re['Result_string'].decode('utf-8')))
except:
try:
print('{}'.format(self.re['Result_string']))
except:
print('printing of results failed.')
if to_file:
with open(self.filename+'_fitting_results','w') as f:
f.write('{}'.format(self.re['Result_string']))
[docs] def Save_project(self, filename=None,path=None):
'''function to dump all the parameter of an analysis into an hdf5 file.
This file contains the ds_ori and all the parameter, including fitting parameter
and results.
One limitation is the fitting model. If the model is build in, so the model is
'exponential' or 'parallel' then the safing works. If an external model is used then the
dostring of the external function is stored, but not the function itself.
Parameters
----------
path : None, str or path, optional
(Default) None, if left on None, then a folder "Data" is created in the folder
of the project (self.path)
filename : str, optional
(Default) None, Base name for all plots. If None, then self.filename will be used
Examples
--------
>>> ta.Save_project()
'''
if filename is None:
filename = self.filename
hdf5_name =check_folder(path = path, current_path = self.path, filename = filename.split('.')[0]+'.hdf5')
if os.path.exists(hdf5_name):
try:
os.remove(hdf5_name)
except:
try:
hdf5_name.close()
os.remove(hdf5_name)
except:
print('File exists but can not be deleted')
re_switch = False
with h5py.File(hdf5_name, 'w') as f:
for key in self.__dict__.keys():
if key == 'mod':
if self.__dict__[key] in ['paral','exponential','consecutive','full_consecutive']:
f.create_dataset(name=key, data=self.__dict__[key])
else:
try:
docstring=self.__dict__[key].__doc__
if isinstance(docstring,type('hello')):
f.create_dataset(name=key, data=docstring)
except:
f.create_dataset(name=key, data='external_function_without_docstring')
elif key in ['rel_wave','rel_time']:#need extra, as it is bypassed by the re-switch
f.create_dataset(name=key, data=self.__dict__[key])
elif key[:2] == 're' :
re_switch = True
for key2 in self.__dict__['re']:
if key2 == 'fit_output':continue
if key2 == 'error':
data = self.__dict__['re'][key2]
try:
f.create_dataset(name='re_error', data=data)
except:
print('saving of ' + key2 + ' failed' )
elif key2 == 'confidence':
for key3 in self.__dict__['re']['confidence'].keys():
try:
f.create_dataset(name='re_confidence_%s_upper'%key3, data=self.__dict__['re'][key2][key3]['upper'])
f.create_dataset(name='re_confidence_%s_lower'%key3, data=self.__dict__['re'][key2][key3]['lower'])
except:
try:
f.create_dataset(name='re_confidence_%s'%key3, data=self.__dict__['re'][key2][key3])
except:
print('saving of' + key3 + 'in confidence failed')
elif isinstance(self.__dict__['re'][key2], pandas.DataFrame):
pass
else:
try:
f.create_dataset(name='re_' + key2, data=self.__dict__['re'][key2])
except:
print('saving of ' + key2 + ' failed' )
elif key == 'cmap':
pass
elif key == 'intensity_range':
data=self.__dict__['intensity_range']
if isinstance(data, type(1e-3)):
data=[-data,data]
if data is None:
f.create_dataset(name='intensity_range', data='None')
else:
f.create_dataset(name='intensity_range', data=data)
elif key == 'background_par':
f.create_dataset(name='back', data=self.__dict__['background_par'][3])
elif key in ['par','par_fit']:
df=par_to_pardf(self.__dict__[key])#pandas has a bug and problems handling mixed type columns when saving. So we clean up.
for sub_key in ['min','max','value']:
try:
df[sub_key]=df[sub_key].astype(float)
except:
pass
df['is_rate']=df['is_rate'].astype(bool)
df['vary']=df['vary'].astype(bool)
df['expr']=df['expr'].apply(lambda x:'%s'%x)
df.to_hdf(str(hdf5_name.resolve()), key=key, append=True, mode='r+', format='t')
else:
data = self.__dict__[key]
if data is None:
f.create_dataset(name=key, data='None')
else:
if isinstance(data, pandas.DataFrame):
pass
else:
try:
f.create_dataset(name=key, data=data)
except:
if key == 'path':
pass
elif key == 'figure_path':
pass
else:
print('the saving of %s failed'%key)
if not 'fitcoeff' in f:
try:
f.create_dataset(name='fitcoeff', data=self.fitcoeff)
except:
try:
with open(self.chirp_file,'r') as f2:
fitcoeff=f2.readline()
f.create_dataset(name='fitcoeff', data=fitcoeff)
except:
pass
self.ds_ori.to_hdf(str(hdf5_name.resolve()), key='ds_ori', append=True, mode='r+', format='t')#save_raw_data
if re_switch:
#print('re-switched')
for key in ['A', 'AC', 'AE', 'DAC', 'c']:
self.re[key].to_hdf(str(hdf5_name.resolve()), key='re_' + key, append=True, mode='r+', format='t')
try:
f.close()
except:
pass
print('The project was saved to %s'%check_folder(path = path, current_path = self.path))
def __read_project(self, saved_project=None,current_path=None):
'''function to re-read all the parameter of a previous analysis
into an hdf5 file, current path is the path that the file should
assume is its "home directory" after successful loading. If not
set we take the filepath at which the file is currently stored as path'''
if saved_project is None:
raise ImportError('We do need a project to import')
if current_path is None:current_path=os.path.dirname(os.path.abspath(saved_project))
try:
import h5py
except:
print('could not import hdf5, current version requires that this is installed. IF running Anaconda open Conda promt and type: conda install h5py')
data_frame_list=[]
# we hav to handle the old and new type of saving
with h5py.File(saved_project, 'r') as f:
if 're_final_setup_par' in f.keys():old_switch=True
else:old_switch=False
if old_switch:print('we read an old style data_file and directly update it into the new file_type after loading')
with h5py.File(saved_project, 'r') as f:
for key in f.keys():
try:
if "re_" in key[:3]:
if not 're' in self.__dict__.keys():
self.__dict__['re']={}
if 're_confidence_' in key:#_upper, _lower
if not 'confidence' in self.__dict__['re']:
self.__dict__['re']['confidence']={}
if '_upper' in key[-6:]:
if not key[14:-6] in self.__dict__['re']['confidence']:
self.__dict__['re']['confidence'][key[14:-6]]={}
self.__dict__['re']['confidence'][key[14:-6]]['upper']=f[key][()]
elif '_lower' in key[-6:]:
if not key[14:-6] in self.__dict__['re']['confidence']:
self.__dict__['re']['confidence'][key[14:-6]]={}
self.__dict__['re']['confidence'][key[14:-6]]['lower']=f[key][()]
else:
self.__dict__['re']['confidence'][key[14:]]=f[key][()]
else:
self.__dict__['re'][key[3:]]=f[key][()]
elif "back" in key[:4]:
rea=f[key][()]
self.__dict__['background_par']=[None,-1,False]
self.__dict__['background_par'].append(rea)
elif "par" in key[:3]:
if old_switch:#old type of saved data
try:
os.remove('temp_file.json')
except:
pass
with open('temp_file.json','w') as g:
g.write(f[key][()])
with open('temp_file.json','r') as g:
self.par=lmfit.Parameters()
self.par.load(g)
try:
os.remove('temp_file.json')
except:
pass
else:#new type of data
raise
else:
read=f[key][()]
if isinstance(read,bytes):
read=f[key].asstr()[()]
if isinstance(read,str):
if (read=='None') or (read=='none'):
read=None
elif key in ['bordercut','timelimits','fitcoeff','scattercut']:
read=[float(a) for a in read]
elif key =='intensity_range':
read=[float(a) for a in read]
elif key in ['rel_time','rel_wave']:
read=np.array(read,dtype=np.float64)
elif key in ['scattercut']:
try:
read=[float(a) for a in read]
except:#maybe we have a list of scattercuts
try:
out_listen=[]
for listen in read:
outlisten.append([float(a) for a in listen])
except:#no idea lets see what happens
pass
self.__dict__[key]=read
except:#we'll get an exception every time there is an dataframe
#print('Frame:'+key)
data_frame_list.append(key)
try:
f.close()
except:
pass
for key in data_frame_list:
try:
if "re_" in key[:3]:
#print('re in list')
self.__dict__['re'][key[3:]]=pandas.read_hdf(saved_project,key=key,mode='r',data_columns=True)
elif key in ['ds_ori','par_fit','par']:
self.__dict__[key]=pandas.read_hdf(saved_project,key=key,mode='r',data_columns=True)
else:
print("missing key:" + key)
except Exception as e:
if key == 'par' and old_switch:pass # we have read it before already and the error is ok
else:
print("error in key:" + key)
print(e)
try:
self.__dict__['re']['fit_results_rates']=self.__dict__['par_fit']
self.__dict__['re']['fit_results_times']=pardf_to_timedf(self.__dict__['re']['fit_results_rates'])
except:
pass
#the par conversion function failed, quickfix
for over_key in ['par_fit','par']:
try:
par_df=self.__dict__[over_key].loc[:,['value','min','max','vary','expr']]
par=lmfit.Parameters()
for key in par_df.index.values:
par.add(key, value=par_df.loc[key,'value'], vary=par_df.loc[key,'vary'], min=par_df.loc[key,'min'], max=par_df.loc[key,'max'])
self.__dict__[over_key]=par
except:
pass
if old_switch:
try:
self.__dict__['re']['fit_results_rates']=par_to_pardf(self.par)
self.__dict__['re']['fit_results_times']=pardf_to_timedf(par_to_pardf(self.par))
self.__dict__['par_fit']=self.par
except:
pass
self.save_figures_to_folder=False
self.path=current_path
if old_switch:#convert project into new type
#clean old files that were read wrong
for key in ['re_final_int_par','re_final_setup_par','re_final_time_par','re_int_error']:
try:
del self.__dict__[key]
except KeyError:
print(f'Key {key} is not in the dictionary')
for key in ['final_int_par','final_setup_par','final_time_par','int_error']:
try:
del self.__dict__['re'][key]
except KeyError:
print(f'Key {key} is not in the dictionary')
try:
self.save_project()
print("project converted into new data type and saved again")
except:
print("project converted ibut could not be saved")
self.path=current_path
for key in ['time_bin','rel_wave','rel_time','scattercut','bordercut','timelimits','intensity_range','wave_nm_bin','wavelength_bin','ignore_time_region']:
try:
if isinstance(self.__dict__[key],bytes):
#print(key + ' set to None')
self.__dict__[key]=None
elif isinstance(self.__dict__[key],str):
if self.__dict__[key]=='None':
self.__dict__[key]=None
except:
continue
try:
self.figure_path=str(self.figure_path)
if 'None' in self.figure_path:
self.figure_path=None
except:
pass
try:
self.re['Result_string']=self.re['Result_string'].decode('utf-8')
except:
pass
[docs] def Copy(self):
'''returns a deep copy of the object.
Examples
--------
>>>ta=plot_func.TA('testfile.hdf5') #open a project
>>>ta1=ta.Copy() #make a copy for some tests or a differnet fit
'''
import copy
return copy.deepcopy(self)
[docs] def Compare_at_time(self, rel_time = None, other = None, fitted = False, norm_window = None,
time_width_percent = None, spectra = None, data_and_fit = False, cmap = None ,
print_click_position = False, linewidth = 1, title='', plot_second_as_energy = True):
'''This function plots multiple spectra into the same figure at a given rel_time (timepoints) and
allows for normalization. Very useful to compare the spectra for different solvents or quenchers, or
e.g. different fits. The ta.time_width_percent parameter defines if this is a
single time (if time_width_percent = 0) or an integrated window.
Only "rel_time" is a mandatory, the rest can be taken from the original project (ta).
The normalization is realized by giving a norm_window
at which the intensity in the triggering object is integrated (in ta.Compare_at_time(other..)
"ta" is the triggering object. The in each of the other curves the same window is
integrated and the curve scaled by this value. Important to note is that this window
does not need to be in the plot. e.g. the normalization can be done at a different time.
Very often one would like to compare the measured spectra at a certain
time to an external spectrum (e.g. spectro-electro-chemistry or steady
state absorption). This can be done by loading a specific spectrum into
a DataFrame and handing this data Frame to the comparision function. The
function can also be used to plot e.g. the measured spectra vs. an
external spectrum without giving any "other" Projects. (very useful for
comparisions).
Parameters
-------------
rel_time : float or list/vector (of floats)
Specify the times where to plot, single value or list/vector of values.
For each entry in rel_time a spectrum is plotted.
If time_width_percent=0 (Default) the nearest measured
timepoint is chosen. For other values see parameter "time_width_percent".
other : TA object or list of those, optional
should be ta.plot_func objects (loaded or copied) and is what
is plotted against the data use a list [ta1,ta2,... ] or generate this
list using the Gui function. See section :ref:`Opening multiple files` in
the documentation
fitted : bool, optional
True/False (Default) - use fitted data instead of raw data.
If True, the fitted datapoints (without interpolation) are used.
This is intended for comparing e.g. different fits
norm_window : None or list/vector (with 4 floats), optional
norm_window Give a list/tupel/vector with 4 entries in the order
[Start - time, End - time, Start - wavelength, End - Wavelength],
see section :ref:`Normalization and Scaling` in the documentation.
If None (Default) no normalization is done.
linewidth : float, optional
linewidth to be used for plotting
time_width_percent : None or float, optional
"rel_time" and "time_width_percent" work together for creating spectral plots at
specific timepoints. For each entry in rel_time a spectrum is plotted.
If however e.g. time_width_percent=10 the region between the timepoint closest
to :math:`timepoint+0.1xtimepoint´ and :math:`timepoint-0.1xtimepoint` is averaged and shown
(and the legend adjusted accordingly). If None (Default) is given, the value is
taken from the triggering object (self.time_width_percent) This is particularly useful for the densly
sampled region close to t=0. Typically for a logarithmic recorded kinetics, the
timepoints at later times will be further appart than 10 percent of the value,
but this allows to elegantly combine values around time=0 for better statistics.
This averaging is only applied for the plotting function and not for the fits.
spectra : None or DataFrame, optional
If an DataFrame with the wavelength as index is provided, Then the spectra of each column
is plotted into the differential spectra 1-1 and the column names are used in the legend
Prior scaling is highly suggested. These spectra are not (in general) scaled with the
norm window. (see examples).
data_and_fit : bool, optional
True or False (Default), choose if for the Fitted plot the raw data of the
other projects is to be plotting in addition to the fitted line. For False (Default)
Only the fit is plotted.
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
plot_second_as_energy : bool, optional
For (Default) True a second x-axis is plotted with "eV" as unit
print_click_position : bool, optional
if True then the click position is printed for the spectral plots
Examples
----------
>>> import plot_func as pf
>>> ta = pf.TA("test1.hdf5") #open the original project
Now open a bunch of other porjects to comare against
>>> other_projects = pf.GUI_open(project_list = ["file1.SIA", "file2.SIA"])
Typical use is compare the raw data without normalization at 1ps and 6ps.
>>> ta.Compare_at_time(rel_time = [1,6], others = other_project)
Compare the fit withput normalization at 1ps and 6ps.
>>> ta.Compare_at_time(rel_time = [1,6], others = other_project, fitted = True)
Compare with normalization window between 1ps and 2ps and 400nm and 450nm.
>>> norm_window=[1,2,400,450]
>>> ta.Compare_at_time(rel_time = [1,6], others = other_project, norm_window = norm_window)
Compare the spectrum at 1ps and 6ps with an external spectrum.
>>> ext_spec = pd.read_csv("Ascii_spectrum.dat", sep = ",")
>>> ta.Compare_at_time(rel_time = [1,6], spectra = ext_spec)
Use example - Often there are a lot of different measurements to
compare at multiple time. The normlization is performed at the ground state bleach
460 nm and early in time. Then it is better to make a new plot for each
timepoint. The normalization window stays fixed.
>>> plt.close("all") #make some space
>>> norm_window=[0.3,0.5,450,470] #define window in ground state bleach
>>> for t in [0.3,0.5,1,3,10,30]: #iterate over the wavelength
>>> ta.Compare_at_time(rel_time = t, others = other_project, norm_window = norm_window)
'''
if self.save_figures_to_folder:self.figure_path=check_folder(path='result_figures',current_path=self.path)
if time_width_percent is None:time_width_percent=self.time_width_percent
if rel_time is None:rel_time=self.rel_time
if other is not None:
if not hasattr(other,'__iter__'):other=[other]
if rel_time is not None:
if not hasattr(rel_time,'__iter__'):rel_time=[rel_time]
else:
rel_time=[1]
if cmap is None:cmap=self.cmap
if fitted:
try:
re=self.re
except:
print("No fitted results present")
return False
if norm_window is not None:
ref_scale=re['A'].loc[norm_window[0]:norm_window[1],norm_window[2]:norm_window[3]].mean().mean()
fig,ax=plt.subplots(figsize=(10,6),dpi=100)
objects=len(rel_time)*(1+len(other))
colors=colm(cmap=cmap,k=range(objects))
_=plot_time(re['A'], ax = ax, rel_time = rel_time, time_width_percent = time_width_percent,
baseunit = self.baseunit, lines_are = 'data', cmap = colors[:len(rel_time)],
title = '', linewidth = linewidth, subplot= True, scattercut = self.scattercut,
plot_second_as_energy = plot_second_as_energy)
_=plot_time(re['AC'], ax = ax, rel_time = rel_time, time_width_percent = time_width_percent,
baseunit = self.baseunit, lines_are = 'fitted', cmap = colors[:len(rel_time)],
title = '', subplot = False, linewidth = linewidth, scattercut = self.scattercut,
plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
lab=['%g %s'%(ent,self.baseunit) + '_' + str(self.filename) for ent in rel_time]
han=handles[:len(rel_time)*2]
for ent in rel_time:
lab.append('%g %s fit'%(ent,self.baseunit) + '_' + str(self.filename))
if other is not None:
for i,o in enumerate(other):
try:
re=o.re
except:
print('%s has no fitted results'%o.filename)
continue
if norm_window is not None:
rel_scale=re['A'].loc[norm_window[0]:norm_window[1],norm_window[2]:norm_window[3]].mean().mean()
try:
scaling=(rel_scale/ref_scale)
ax=plot_time(re['AC']/scaling, cmap = colors, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = '',
lines_are = 'fitted', subplot = True,
color_offset = len(rel_time)*(i+1), linewidth = linewidth,
scattercut = o.scattercut,plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
for ent in rel_time:
lab.append('%g %s fit'%(ent,o.baseunit) + '_' + str(o.filename))
for a in handles[-len(rel_time):]:
han.append(a)
if data_and_fit:
ax=plot_time(re['A']/scaling, cmap = self.cmap, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = o.filename,
baseunit = self.baseunit, lines_are = 'data', subplot = True,
color_offset = len(rel_time)*(i+1), linewidth = linewidth,
scattercut = o.scattercut,plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
for ent in rel_time:
lab.append('%g %s'%(ent,o.baseunit) + '_' + str(o.filename))
for a in handles[-len(rel_time):]:
han.append(a)
norm_failed = False
except:
print('scaling Failed!')
norm_failed = True
else: norm_failed=True
if norm_failed:
ax=plot_time(re['AC'], cmap = colors, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = '',
lines_are = 'fitted', subplot = True,
color_offset = len(rel_time)*(i+1), linewidth = linewidth,
scattercut = o.scattercut, plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
for ent in rel_time:
lab.append('%g %s fit'%(ent,o.baseunit) + '_' + str(o.filename))
for a in handles[-len(rel_time):]:
han.append(a)
if data_and_fit:
ax=plot_time(re['A'], cmap = self.cmap, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = o.filename,
baseunit = self.baseunit, lines_are = 'data', subplot = True,
color_offset = len(rel_time)*(i+1),
linewidth = linewidth, scattercut = o.scattercut,
plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
for ent in rel_time:
lab.append('%g %s'%(ent,o.baseunit) + '_' + str(o.filename))
for a in handles[-len(rel_time):]:
han.append(a)
if not norm_failed:
ax.set_title('compare measured and fitted data at given times\n scaled to t=%g ps : %g ps , wl= %g nm: %g nm'%(norm_window[0],norm_window[1],norm_window[2],norm_window[3]))
else:
ax.set_title('compare measured and fitted data at given times')
ax.set_xlim(re['A'].columns.values[0],re['A'].columns.values[-1])
ax.legend(han, lab ,labelspacing = 0, ncol = 2, columnspacing = 1, handlelength = 1, frameon = False)
else:
if norm_window is not None:
ref_scale=self.ds.loc[norm_window[0]:norm_window[1],norm_window[2]:norm_window[3]].mean().mean()
objects=len(rel_time)*(1+len(other))
colors=colm(cmap=cmap,k=range(objects))
fig,ax=plt.subplots(figsize=(10,6),dpi=100)
_=plot_time(self.ds, ax = ax, rel_time = rel_time, time_width_percent = time_width_percent,
title = title, lines_are = 'data', scattercut = self.scattercut,
bordercut = self.bordercut, wave_nm_bin = self.wave_nm_bin, cmap = colors,
subplot = True, linewidth = linewidth, baseunit=self.baseunit,
plot_second_as_energy = plot_second_as_energy)
if 1:
_=plot_time(self.ds, ax = ax, rel_time = rel_time, time_width_percent = time_width_percent,
title = title, lines_are = 'smoothed', scattercut = self.scattercut,
bordercut = self.bordercut,wave_nm_bin = self.wave_nm_bin, cmap = colors,
subplot = False, linewidth = linewidth, baseunit = self.baseunit,
plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
lab=['%g %s'%(ent,self.baseunit) + '_' + str(self.filename) for ent in rel_time]
han=handles[:len(rel_time)]
if other is not None:
for i,o in enumerate(other):
if norm_window is not None:
rel_scale=o.ds.loc[norm_window[0]:norm_window[1],norm_window[2]:norm_window[3]].mean().mean()
try:
scaling = (rel_scale/ref_scale)
ax=plot_time(o.ds/scaling, cmap = colors, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = title,
lines_are = 'data', scattercut = o.scattercut,
bordercut = o.bordercut, linewidth = linewidth,
wave_nm_bin = o.wave_nm_bin, subplot = True,
color_offset = len(rel_time)*(i+1),
plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
for ent in rel_time:
lab.append('%g %s'%(ent,o.baseunit) + '_' + str(o.filename))
for a in handles[-len(rel_time):]:
han.append(a)
if data_and_fit:
ax=plot_time(o.ds/scaling, cmap = colors, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = title,
lines_are = 'smoothed', scattercut = o.scattercut,
bordercut = o.bordercut, linewidth = linewidth,
wave_nm_bin = o.wave_nm_bin, subplot = True,
color_offset = len(rel_time)*(i+1),
plot_second_as_energy = plot_second_as_energy)
scaling_failed=False
except:
print('scaling Failed!')
scaling_failed=True
else:
scaling_failed=True
if scaling_failed:
ax=plot_time(o.ds, cmap = colors, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = title,
lines_are = 'data', scattercut = o.scattercut,
bordercut = o.bordercut, linewidth = linewidth,
wave_nm_bin = o.wave_nm_bin, subplot = True,
color_offset = len(rel_time)*(i+1),
plot_second_as_energy = plot_second_as_energy)
handles, labels=ax.get_legend_handles_labels()
for ent in rel_time:
lab.append('%g %s'%(ent,o.baseunit) + '_' + str(o.filename))
for a in handles[-len(rel_time):]:
han.append(a)
if data_and_fit:
ax=plot_time(o.ds, cmap = colors, ax = ax, rel_time = rel_time,
time_width_percent = time_width_percent, title = title,
lines_are = 'smoothed', scattercut = o.scattercut,
bordercut = o.bordercut, linewidth = linewidth,
wave_nm_bin = o.wave_nm_bin, subplot = True,
color_offset = len(rel_time)*(i+1),
plot_second_as_energy = plot_second_as_energy)
if not scaling_failed:
ax.set_title('compare measured and smoothed data at given times\n scaled to t=%g ps : %g ps , wl= %g nm: %g nm'%(norm_window[0],norm_window[1],norm_window[2],norm_window[3]))
else:
ax.set_title('compare measured and smoothed data at given times')
ax.legend(han, lab, labelspacing = 0, ncol = 2, columnspacing = 1, handlelength = 1, frameon = False)
if self.bordercut is None:
ax.set_xlim(self.ds.columns.values[0],self.ds.columns.values[-1])
else:
ax.set_xlim(self.bordercut)
if spectra is not None:
spectra.plot(ax=ax,legend=False)
handles, labels=ax.get_legend_handles_labels()
han.append(handles[-1])
lab.append(labels[-1])
ax.legend(han, lab ,labelspacing = 0, ncol = 2, columnspacing = 1, handlelength = 1, frameon = False)
if plot_second_as_energy:
ax2=ax.twiny()
ax2.set_xlim(ax.get_xlim())
ax2.set_xticks(ax.get_xticks())
labels=['%.2f'%(scipy.constants.h*scipy.constants.c/(a*1e-9*scipy.constants.electron_volt)) for a in ax2.get_xticks()]
_=ax2.set_xticklabels(labels)
_=ax2.set_xlabel('Energy in eV')
ax.set_zorder(ax2.get_zorder()+1)
fig=plt.gcf()
fig.tight_layout()
if self.save_figures_to_folder:
fig.savefig(check_folder(path=self.figure_path,filename='compare_at_time_%s.png'%'_'.join(['%g'%a for a in rel_time])),bbox_inches='tight')
[docs] def Compare_at_wave(self, rel_wave = None, other = None, fitted = False, norm_window = None,
width = None, cmap = None, data_and_fit = False, scale_type = 'symlog', linewidth = 1):
'''This function plots multiple kinetics into the same figure at one or
multiple given wavelength (rel_wave) and allows for
:ref:`Normalization and Scaling` Very useful to compare the
kinetics for different quencher concentrations or pump powers,
or e.g. different fits. The parameter width or the general self.wavelength_bin
which is used if width is None (Default) defines the width of
the spectral window that is integrated and shown.
A normalization window can be given at which all the plotted curves are normalized to.
This window does not have to be in the plotted region. See :ref:`Normalization and Scaling`
Parameters
--------------
rel_wave : float or list/vector (of floats)
Specify the wavelength where to plot the kinetics, single value or
list/vector of values (only mandatory entry) For each entry in
rel_wave a kinetic is plotted. 'rel_wave' and 'width'
(in the object called 'wavelength_bin' work together for the creation
of kinetic plots. At each selected wavelength the data between
wavelength+width/2 and wavelength-width/2 is averaged
for each timepoint
other : TA object or list of those, optional
should be ta.plot_func objects (loaded or copied) and is what
is plotted against the data use a list [ta1,ta2,... ] or generate this
list using the Gui function. See section :ref:`Opening multiple files` in
the documentation
fitted : bool, optional
True/False (Default) - use fitted data instead of raw data.
If True, the fitted datapoints (without interpolation) are used.
This is intended for comparing e.g. different fits
norm_window : None or list/vector (with 4 floats), optional
norm_window Give a list/tupel/vector with 4 entries in the order
[Start - time, End - time, Start - wavelength, End - Wavelength],
see section :ref:`Normalization and Scaling` in the documentation.
If None (Default) no normalization is done.
width
Specify the width above and below the given wavelength that is
integrated as window. If left to (Default) "None" the value from ta is
used.
data_and_fit : bool, optional
True or False (Default), choose if for the Fitted plot the raw data of the
other projects is to be plotting in addition to the fitted line. For False (Default)
Only the fit is plotted.
linewidth : float, optional
linewidth to be used for plotting
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
Scale_type : None or str
is a general setting that can influences what time axis will be used for the plots.
"symlog" (linear around zero and logarithmic otherwise) "lin" and "log" are valid options.
Examples
--------
>>> import plot_func as pf
>>> ta = pf.TA('test1.hdf5') #open the original project
Now open a bunch of other projects to compare against
>>> other_projects = pf.GUI_open(project_list = ['file1.SIA', 'file2.SIA'])
Typical use:
Compare the raw data without normalization at 400 nm and 500 nm
>>> ta.Compare_at_wave(rel_wave = [400, 500], others = other_project)
Compare the quality of the fit data without normalization at 400 nm and 500 nm
>>> ta.Compare_at_wave(rel_wave = [400, 500], others = other_project, fitted = True)
Compare with normalization window between 1ps and 2ps and 400nm and 450nm
>>> norm_window=[1,2,400,450]
>>> ta.Compare_at_wave(rel_wave = [400, 500], others = other_project, norm_window = norm_window)
Use example: Often there are a lot of different measurements to
compare at multiple wavelength. The normlization is performed at the ground state bleach
460 nm and early in time. Then it is better to make a new plot for each
wavelength. The normalization window stays fixed.
>>> plt.close('all') #make some space
>>> norm_window=[0.3,0.5,450,470] #define window in ground state bleach
>>> for wave in [300,400,500,600,700]: #iterate over the wavelength
>>> ta.Compare_at_wave(rel_wave = wave, others = other_project, norm_window = norm_window)
'''
if self.save_figures_to_folder:self.figure_path=check_folder(path='result_figures',current_path=self.path)
if width is None:width=self.wavelength_bin
if rel_wave is None:
rel_wave=self.rel_wave
if other is not None:
if not hasattr(other,'__iter__'):other=[other]
if not hasattr(rel_wave,'__iter__'):
rel_wave=[rel_wave]
if cmap is None:cmap=self.cmap
if fitted:
try:
re=self.re
except:
print("No fitted results present")
return False
if norm_window is not None:
ref_scale = re['A'].loc[norm_window[0]:norm_window[1], norm_window[2]:norm_window[3]].mean().mean()
fig, ax = plt.subplots(figsize = (10, 6), dpi = 100)
colors = colm(cmap = cmap, k = range(len(rel_wave)*(2+len(other))))
ax = plot1d(re['A'], ax = ax, wavelength = rel_wave, width = width, lines_are = 'data',
cmap = colors, title = '', plot_type = scale_type, linewidth = linewidth)
ax = plot1d(re['AC'], ax = ax, wavelength = rel_wave, width = width, lines_are = 'fitted',
cmap = colors, title = '', subplot = True, plot_type = scale_type, linewidth = linewidth)
#ax = plot1d(re['AC'], ax = ax, wavelength = rel_wave, width = width, lines_are = 'fitted',
# cmap = colors, color_offset = len(rel_wave), title = '', subplot = True, plot_type = scale_type)
hand, labels = ax.get_legend_handles_labels()
lab=['%g nm'%a + '_' + str(self.filename) for a in rel_wave]
for ent in rel_wave:
lab.append('%g nm'%ent + '_' + str(self.filename))
if other is not None:
for i,o in enumerate(other):
i+=1
color_offset=(i+1)*len(rel_wave)
try:
re=o.re
except:
print('%s has no fitted results'%o.filename)
continue
if norm_window is not None:
rel_scale=re['A'].loc[norm_window[0]:norm_window[1],norm_window[2]:norm_window[3]].mean().mean()
try:
scaling=(rel_scale / ref_scale)
if data_and_fit:
ax = plot1d(re['A']/scaling, cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = '', lines_are = 'data', subplot = True, color_offset = color_offset,
plot_type = scale_type, linewidth = linewidth)
ax = plot1d(re['AC']/scaling, cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = '', lines_are = 'fitted', subplot = True, color_offset = color_offset,
plot_type = scale_type, linewidth = linewidth)
except:
print('scaling Failed!')
if data_and_fit:
ax = plot1d(re['A'], cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = '', lines_are = 'data', subplot = True, color_offset = color_offset,
plot_type = scale_type, linewidth = linewidth)
ax = plot1d(re['AC'], cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = '', lines_are = 'fitted', subplot = True, color_offset = color_offset,
plot_type = scale_type, linewidth = linewidth)
else:
if data_and_fit:
ax = plot1d(re['A'], cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = '', lines_are = 'data', subplot = True, color_offset = color_offset,
plot_type = scale_type, linewidth = linewidth)
ax = plot1d(re['AC'], cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = '', lines_are = 'fitted', subplot = True, color_offset = color_offset,
plot_type = scale_type, linewidth = linewidth)
for ent in rel_wave:
if data_and_fit:
lab.append('%g nm'%ent + '_' + str(o.filename))
lab.append('%g nm'%ent + '_' + str(o.filename))
handles, labels=ax.get_legend_handles_labels()
if data_and_fit:
for a in handles[-2*len(rel_wave):]:
hand.append(a)
else:
for a in handles[-len(rel_wave):]:
hand.append(a)
if norm_window is not None:
ax.set_title('compare measured and fitted data at given wavelength \n scaled to t=%g ps : %g ps , wl= %g nm: %g nm'%(norm_window[0],norm_window[1],norm_window[2],norm_window[3]))
else:
ax.set_title('compare measured and fitted data at given wavelength')
ax.set_xlim(re['A'].index.values[0],re['A'].index.values[-1])
ax.legend(hand,lab)
else:
fig, ax = plt.subplots(figsize = (10, 6), dpi = 100)
colors = colm(cmap = cmap, k = range(len(rel_wave)*(2+len(other))))
if norm_window is not None:
ref_scale = self.ds.loc[norm_window[0]:norm_window[1], norm_window[2]:norm_window[3]].mean().mean()
ax = plot1d(self.ds, cmap = colors, ax = ax, wavelength = rel_wave, width = width, title = self.filename,
baseunit = self.baseunit, lines_are = 'data', scattercut = self.scattercut,
bordercut = self.bordercut, subplot = False, color_offset = 0, timelimits = self.timelimits,
intensity_range = self.intensity_range, plot_type = scale_type, linewidth = linewidth)
ax = plot1d(self.ds, cmap = colors, ax = ax, wavelength = rel_wave, width = width, title = self.filename,
baseunit = self.baseunit, lines_are = 'smoothed', scattercut = self.scattercut,
bordercut = self.bordercut, subplot = False, color_offset = 0, timelimits = self.timelimits,
intensity_range = self.intensity_range, plot_type = scale_type, linewidth = linewidth)
if 1:
handles, labels=ax.get_legend_handles_labels()
lab=['%g nm'%a + '_' + str(self.filename) for a in rel_wave]
hand=handles[len(rel_wave):]
if other is not None:
for i,o in enumerate(other):
i+=1
color_offset=(i+1)*len(rel_wave)
if norm_window is not None:
rel_scale=o.ds.loc[norm_window[0]:norm_window[1],norm_window[2]:norm_window[3]].mean().mean()
try:
scaling=(rel_scale/ref_scale)
ax = plot1d(o.ds/scaling, cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = o.filename, baseunit = self.baseunit, timelimits = self.timelimits,
lines_are = 'data', scattercut = self.scattercut, bordercut = self.bordercut,
subplot = True, color_offset = color_offset,
intensity_range = self.intensity_range, plot_type = scale_type, linewidth = linewidth)
ax = plot1d(o.ds/scaling, cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = o.filename, baseunit = self.baseunit, timelimits = self.timelimits,
lines_are = 'smoothed', scattercut = self.scattercut, bordercut = self.bordercut,
subplot = True, color_offset = color_offset,
intensity_range = self.intensity_range, plot_type = scale_type, linewidth = linewidth)
except:
print('scaling failed')
ax = plot1d(o.ds, cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = o.filename, baseunit = self.baseunit, timelimits = self.timelimits,
lines_are = 'data', scattercut = self.scattercut, bordercut = self.bordercut,
subplot = True, color_offset = color_offset,
intensity_range = self.intensity_range, plot_type = scale_type, linewidth = linewidth)
ax = plot1d(o.ds, cmap = colors, ax = ax, wavelength = rel_wave, width = width,
title = o.filename, baseunit = self.baseunit, timelimits = self.timelimits,
lines_are = 'smoothed', scattercut = self.scattercut, bordercut = self.bordercut,
subplot = True, color_offset = color_offset,
intensity_range = self.intensity_range, plot_type = scale_type, linewidth = linewidth)
else:
ax = plot1d(o.ds, cmap = colors, ax = ax, wavelength = rel_wave, width = width, title = o.filename,
baseunit = self.baseunit, timelimits = self.timelimits, lines_are = 'data',
scattercut = self.scattercut, bordercut = self.bordercut, subplot = True,
color_offset = color_offset, intensity_range = self.intensity_range,
plot_type = scale_type, linewidth = linewidth)
ax = plot1d(o.ds, cmap = colors, ax = ax, wavelength = rel_wave, width = width, title = o.filename,
baseunit = self.baseunit, timelimits = self.timelimits, lines_are = 'smoothed',
scattercut = self.scattercut, bordercut = self.bordercut, subplot = True,
color_offset = color_offset, intensity_range = self.intensity_range,
plot_type = scale_type, linewidth = linewidth)
for ent in ['%g nm'%a + '_' + str(o.filename) for a in rel_wave]:
lab.append(ent)
handles, labels=ax.get_legend_handles_labels()
for ha in handles[-len(rel_wave):]:
hand.append(ha)
ax.set_title('compare measured and smoothed data at given wavelength')
if norm_window is not None:
ax.set_title('compare measured and smoothed data at given wavelength \n scaled to t=%g ps : %g ps , wl= %g nm: %g nm'%(norm_window[0],norm_window[1],norm_window[2],norm_window[3]))
ax.legend(hand,lab)
if self.save_figures_to_folder:
fig.savefig(check_folder(path=self.figure_path,filename='compare_at_wave_%s.png'%'_'.join(['%g'%a for a in rel_wave])),
bbox_inches='tight')
return ax
[docs] def Compare_DAC(self, other = None, spectra = None, separate_plots = False, cmap = None):
'''This is a convenience function to plot multiple extracted spectra
(DAS or species associated) into the same figure or into a separate figure
each. Other should be ta.plot_func objects (loaded or copied). By
standard it plots all into the same window. If all project have the same
number of components one can activate "separate_plots" and have each
separated (in the order created in the projects).
The "Spectra" parameter allows as before the inclusion of an external
spectrum. Others is optional and I use this function often to compare
species associated spectra with one or multiple steady state spectra.
Parameters
--------------
other : TA object or list of those, optional
should be ta.plot_func objects (loaded or copied) and is what
is plotted against the data use a list [ta1,ta2,... ] or generate this
list using the Gui function. See section :ref:`Opening multiple files` in
the documentation
spectra : None or DataFrame, optional
If an DataFrame with the wavelength as index is provided, Then the spectra of each column
is plotted into the differential spectra 1:1 and the column names are used in the legend
Prior scaling is highly suggested. These spectra are not (in general) scaled with the
norm window. (see examples)
separate_plots : bool, optional
True or False (Default), separate plots is the switch that decides if a axis or
multiple axis are used. This option will result in a crash unless all objects have the
same number of DAS/SAS components
cmap : None or matplotlib color map, optional
is a powerfull variable that chooses the colour map applied for all plots. If set to
None (Default) then the self.cmap is used.
As standard I use the color map "jet" from matplotlib. There are a variety of colormaps
available that are very usefull. Beside "jet", "viridis" is a good choice as it is well
visible under red-green blindness. Other useful maps are "prism" for high fluctuations
or diverging color maps like "seismic".
See https://matplotlib.org/3.1.0/tutorials/colors/colormaps.html for a comprehensive
selection. In the code the colormaps are imported so if plot_func is imported as pf then
self.cmap=pf.cm.viridis sets viridis as the map to use. Internally the colors are chosen
with the "colm" function. The 2d plots require a continuous color map so if something
else is give 2d plots are shown automatically with "jet". For all of the 1d plots however
I first select a number of colors before each plot. If cmap is a continous map then these
are sampled evenly over the colourmap. Manual iterables of colours
cmap=[(1,0,0),(0,1,0),(0,0,1),...] are also accepted, as are vectors or dataframes that
contain as rows the colors. There must be of course sufficient colors present for
the numbers of lines that will be plotted. So I recommend to provide at least 10 colours
(e.g.~your university colors). colours are always given as a, list or tuple with RGA or RGBA
(with the last A beeing the Alpha=transparency. All numbers are between 0 and 1.
If a list/vector/DataFrame is given for the colours they will be used in the order provided.
Examples
--------
>>> import plot_func as pf
>>> ta = pf.TA('test1.hdf5') #open the original project,
>>> this MUST contain a fit, otherwise this will raise an error
Now open a bunch of other projects to compare against,
>>> #compare in a single window
>>> other_projects = pf.GUI_open(project_list = ['file1.hdf5', 'file2.hdf5'])
>>> ta.Compare_DAC(others = other_project)
>>> #comprare in separate windows,
>>> #the other projects must have the same number of components
>>> ta.Compare_DAC(others = other_project, separate_plots = True)
Compare the DAC to an external spectrum
>>> ext_spec = pd.read_csv('Ascii_spectrum.dat', sep = ',')
>>> ta.Compare_DAC(spectra = ext_spec) #compare just the current solution
>>> ta.Compare_DAC(spectra = ext_spec, others = other_project) #compare multiple
'''
if self.save_figures_to_folder:self.figure_path = check_folder(path = 'result_figures', current_path = self.path)
if other is not None:
if not hasattr(other, '__iter__'):other = [other]
try:
re = self.re.copy()
except:
print("No fitted results present")
return False
if cmap is None:cmap = self.cmap
species=re['DAC'].columns.values
if other is None:
col = range(len(re['DAC'].columns.values))
colors = colm(cmap = cmap, k = col)
else:
re['DAC'].columns = [self.filename + '\n' + '%s'%a for a in re['DAC'].columns]
if separate_plots:
colors = colm(cmap = cmap, k = np.arange(len(other)+1))
else:
colors = colm(cmap = cmap, k = np.arange((len(other)+1)*len(species)))
DAC = re['DAC']
hand=[]
if separate_plots:
n_cols = int(np.ceil(len(re['DAC'].columns)/2))
col = [colors[0] for a in range(len(re['DAC'].columns))]
if self.scattercut is None:
ax = DAC.plot(subplots = separate_plots, figsize = (12, 10), layout = (n_cols, 2),
legend = False, color = col, sharex = False)
a=ax.ravel()
handles,labels=a[0].get_legend_handles_labels()
hand.append(handles[-1])
elif isinstance(self.scattercut[0], numbers.Number):
ax = DAC.loc[:self.scattercut[0], :].plot(subplots = separate_plots, figsize = (12, 10), layout = (n_cols, 2),
legend = False, color = col, sharex = False)
a=ax.ravel()
handles,labels=a[0].get_legend_handles_labels()
hand.append(handles[-1])
DAC_cut=DAC.loc[self.scattercut[1]:, :]
for i,am in enumerate(DAC_cut.columns):
DAC_cut.iloc[:,i].plot(ax = a[i], legend = False, color = col)
else:
scattercut = flatten(self.scattercut)
for i in range(len(scattercut)/2+1):
if i == 0:
ax = DAC.loc[:scattercut[0], :].plot(subplots = separate_plots, figsize = (12, 10),
layout = (n_cols, 2), legend = False, color = col, sharex = False)
a=ax.ravel()
handles,labels=a[0].get_legend_handles_labels()
hand.append(handles[-1])
elif i<(len(scattercut)/2):
for j,am in enumerate(ax):
DAC.loc[scattercut[2*i-1]:scattercut[2*i], :].plot(ax = a[j], legend = False, color = col, label = '_nolegend_')
else:
for j,am in enumerate(ax):
DAC.loc[scattercut[-1]:, :].plot(ax = a[j], legend = False, color = col, label = '_nolegend_')
else:
if self.scattercut is None:
ax = DAC.plot(subplots = separate_plots, figsize = (16, 8), legend = False, color = colors[:len(species)], label = '_nolegend_')
elif isinstance(self.scattercut[0], numbers.Number):
ax = DAC.loc[:self.scattercut[0], :].plot(subplots = separate_plots, figsize = (16, 8), legend = False, color = colors[:len(species)], label = '_nolegend_')
ax = DAC.loc[self.scattercut[1]:, :].plot(ax=ax, subplots = separate_plots, figsize = (16, 8), legend = False, color = colors[:len(species)], label = '_nolegend_')
else:
scattercut = flatten(self.scattercut)
for i in range(len(scattercut)/2+1):
if i == 0:
ax = DAC.loc[:scattercut[0], :].plot(subplots = separate_plots, figsize = (16, 8), legend = False, color = colors[:len(species)], label = '_nolegend_')
elif i<(len(scattercut)/2):
ax = DAC.loc[scattercut[2*i-1]:scattercut[2*i], :].plot(ax=ax, subplots = separate_plots, figsize = (16, 8), legend = False, color = colors[:len(species)], label = '_nolegend_')
else:
ax = DAC.loc[scattercut[-1]:, :].plot(ax=ax, subplots = separate_plots, figsize = (16, 8), legend = False, color = colors[:len(species)], label = '_nolegend_')
if other is not None:
for i,o in enumerate(other):
try:
re=o.re.copy()
except:
print('%s has no fitted results'%o.filename)
continue
re['DAC'].columns=[o.filename + '\n' + '%s'%a for a in re['DAC'].columns]
if separate_plots:
col=[colors[i+1] for a in range(len(re['DAC'].columns))]
for j,am in enumerate(re['DAC'].columns):
if o.scattercut is None:
re['DAC'].iloc[:,j].plot(subplots=False,ax=a[j],legend=False,color=col[i])
if j==0:
handles,labels=a[0].get_legend_handles_labels()
hand.append(handles[-1])
elif isinstance(o.scattercut[0], numbers.Number):
re['DAC'].iloc[:,j].loc[:o.scattercut[0]].plot(subplots=False,ax=a[j],legend=False,color=col[i])
if j==0:
handles,labels=a[0].get_legend_handles_labels()
hand.append(handles[-1])
re['DAC'].iloc[:,j].loc[o.scattercut[1]:].plot(subplots=False,ax=a[j],legend=False,color=col[i],label = '_nolegend_')
else:
scattercut = flatten(o.scattercut)
for m in range(len(scattercut)/2+1):
if m == 0:
re['DAC'].iloc[:,j].loc[:scattercut[0]].plot(subplots=False,ax=a[j],legend=False,color=col[i])
if j==0:
handles,labels=a[j].get_legend_handles_labels()
hand.append(handles[-1])
elif m<(len(scattercut)/2):
re['DAC'].iloc[:,j].loc[scattercut[2*m-1]:scattercut[2*m]].plot(subplots=False,ax=a[j],legend=False,color=col[i],label = '_nolegend_')
else:
re['DAC'].iloc[:,j].loc[scattercut[-1]:].plot(subplots=False,ax=a[j],legend=False,color=col[i],label = '_nolegend_')
a[j].set_xlabel('Wavelength in nm')
a[j].set_ylabel('Spectral strength in arb. units')
a[j].legend(fontsize=8,frameon=False)
else:
dacs=len(re['DAC'].columns)
col=colors[(i+1)*dacs:(i+2)*dacs]
DAC=re['DAC']
if o.scattercut is None:
ax = DAC.plot(subplots=separate_plots,ax=ax,legend=False,color=colors[(i+1)*len(species):(i+2)*len(species)])
elif isinstance(o.scattercut[0], numbers.Number):
ax = DAC.loc[:o.scattercut[0], :].plot(subplots=separate_plots,ax=ax,legend=False,color=colors[(i+1)*len(species):(i+2)*len(species)])
DAC.loc[o.scattercut[1]:, :].plot(subplots=separate_plots,ax=ax,legend=False,color=colors[(i+1)*len(species):(i+2)*len(species)])
else:
scattercut = flatten(o.scattercut)
for i in range(len(scattercut)/2+1):
if i == 0:
ax = DAC.loc[:scattercut[0], :].plot(subplots=separate_plots,ax=ax,legend=False,color=colors[(i+1)*len(species):(i+2)*len(species)])
elif i<(len(scattercut)/2):
ax = DAC.loc[scattercut[2*i-1]:scattercut[2*i], :].plot(subplots=separate_plots,ax=ax,legend=False,color=colors[(i+1)*len(species):(i+2)*len(species)])
else:
ax = DAC.loc[scattercut[-1]:, :].plot(subplots=separate_plots,ax=ax,legend=False,color=colors[(i+1)*len(species):(i+2)*len(species)])
ax.set_xlabel('Wavelength in nm')
ax.set_ylabel('Spectral strength in arb. units')
ax.legend(fontsize=8,frameon=False)
if not hasattr(ax,'__iter__'):ax=np.array([ax])
if spectra is not None:
for a in ax:
spectra.plot(ax=a,subplots=separate_plots)
fig=(ax.ravel())[0].figure
if separate_plots:
fig.set_size_inches(12,10)
axes_number=fig.get_axes()
names=[self.filename]
if other is not None:
for o in other:
names.append(o.filename)
for i,ax in enumerate(axes_number):
try:
nametemp=['%s'%species[i] + ' - ' + a for a in names]
ax.legend(hand,nametemp)
except:
pass
else:
ax=fig.get_axes()[0]
names=[self.filename]
if other is not None:
for o in other:
names.append(o.filename)
handles,labels=ax.get_legend_handles_labels()
nametemp=[]
try:
for a in names:
for b in species:
nametemp.append('%s'%b + ' - ' + a)
ax.legend(handles,nametemp)
except:
pass
fig.set_size_inches(16,8)
fig.tight_layout()
if self.save_figures_to_folder:
fig.savefig(check_folder(path=self.figure_path,filename='compare_DAC.png'),bbox_inches='tight')