Part 4: Linear combination fitting

by Morgane Desmau & Marco Alsina

Last update: June 2021

The following notebook explains the following:

  1. Perform linear combination fitting (LCF) on XANES spectra.

  2. Perform LCF on EXAFS spectra.

Important: This tutorial assumes you have succesfully completed the previous tutorials in the series:

[1]:
# checking version of araucaria and dependencies
from araucaria.utils import get_version
print(get_version(dependencies=True))
Python version      : 3.9.4
Numpy version       : 1.20.3
Scipy version       : 1.6.3
Lmfit version       : 1.0.2
H5py version        : 3.2.1
Matplotlib version  : 3.4.2
Araucaria version   : 0.1.10

1. Accesing the database

In this case we will be reading and processing a minerals database measured at the Fe K-edge in the P65 beamline of DESY, Hamburg (data kindly provided by Morgane Desmau):

  1. Fe_database.h5

We first retrieve the filepath to the database and summarize its contents.

Note

If you prefer to process your own database, just modify the filepath to point to the location of your file.

[2]:
from pathlib import Path
from araucaria.testdata import get_testpath
from araucaria.io import summary_hdf5

# retrieving filepath
fpath = get_testpath('Fe_database.h5')

# summarizing database
report = summary_hdf5(fpath)
report.show()
=================================
id  dataset           mode    n
=================================
1   FeIISO4_20K       mu      5
2   Fe_Foil           mu_ref  5
3   Ferrihydrite_20K  mu      5
4   Goethite_20K      mu      5
=================================

2. Creating a group to fit

We will use the merge() function to create an example signal to fit, in this case a mixture of the 2 original signals from ferrihydrite and goethite minerals.

Note that the original signals are multiplied by normalized amplitude factors to prevent proportions larger than 1 during the fit.

We will store the original signals and the mixture in a Collection, compute normalized spectra through the apply() method, and produce a summary report.

[3]:
from araucaria.io import read_hdf5
from araucaria.xas import merge, pre_edge, autobk
from araucaria import Collection

# name of groups
groupnames = ('Ferrihydrite_20K', 'Goethite_20K')
amp        = [0.5, 0.5]

# initializing collections
collection = Collection()
auxiliary  = Collection()

# reading scans and adding them to collection
for i,name in enumerate(groupnames):
    group = read_hdf5(fpath, name)
    collection.add_group(group, tag='ref')
    group.mu = amp[i]*group.mu
    auxiliary.add_group(group)

# merging scans
report, merge = merge(auxiliary, name='sample')
collection.add_group(merge, tag='scan')

# normalizing spectra in collection
collection.apply(pre_edge)

# printing report
report = collection.summary(optional=['edge_step'])
report.show()
================================================
id  dataset           tag   mode  n  edge_step
================================================
1   Ferrihydrite_20K  ref   mu    5  0.27895
2   Goethite_20K      ref   mu    5  0.4038
3   sample            scan  mu    2  0.34112
================================================

3. Computing expected amplitudes from the LCF

As seen from the summary report, the edge steps of the original signals are not equal. We can compute the expected proportions by re-normalizing the amplitudes considering the proportion of the edge step values:

[4]:
from numpy import sum

edge_steps = report.get_cols(names=['edge_step'], astype='float')[:2]
amplitudes = [amp[i]*val for i, val in enumerate(edge_steps)]
amplitudes = [val/sum(amplitudes) for val in amplitudes]

for i, val in enumerate(amplitudes):
    print('{0:16}: {1:1.3f}'.format(groupnames[i], val))
Ferrihydrite_20K: 0.409
Goethite_20K    : 0.591

4. Performing XANES LCF

We can perform LCF with the lcf() function. For clarity we use dictionaries to specify the normalization and lcf parameters for the fit. Note that we specify fit_region='xanes' and a fit_range with respect to a fixed absorption threshold in energy units.

Once the LCF is finished we can print a summary with the lcf_report() function.

[5]:
from araucaria.fit import lcf, lcf_report

# parameters for normalization and lcf
k_edge = 7112
pre_edge_kws = {'pre_range' : [-160, -40],
                'post_range': [140, 950],
                'nvict'     : 0,
                'e0'        : k_edge}

lcf_kws      = {'fit_region': 'xanes',
                'sum_one'   : False,
                'fit_range' : [k_edge-20,k_edge+70]}

collection.apply(pre_edge, **pre_edge_kws)
out = lcf(collection, **lcf_kws)
print(lcf_report(out))
[[Parameters]]
    fit_region         = xanes
    fit_range          = [7092, 7182]
    sum_one            = False
[[Groups]]
    scan               = sample
    ref1               = Ferrihydrite_20K
    ref2               = Goethite_20K
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 10
    # data points      = 168
    # variables        = 2
    chi-square         = 1.5177e-07
    reduced chi-square = 9.1428e-10
    Akaike info crit   = -3494.57580
    Bayesian info crit = -3488.32787
[[Variables]]
    amp1:  0.40894498 +/- 9.9474e-05 (0.02%) (init = 0.5)
    amp2:  0.59225273 +/- 9.9337e-05 (0.02%) (init = 0.5)
[[Correlations]] (unreported correlations are < 0.100)
    C(amp1, amp2) = -1.000

As seen in the report, our prediction for the amplitudes of the original signals is in agreement with the LCF results.

We can visually compare the fitted curve with the original signal with the fig_lcf() function. The plot function accepts a dictionary to set the parameters for the XAFS figures (fig_pars), as well as a dictionary to set general figure parameters (fig_kws).

[6]:
import matplotlib.pyplot as plt
from araucaria.plot import fig_lcf

# figure parameters
offset   = 0.5
fig_kws  = {'figsize'   : (8, 5)}  # size figure
fig_pars = {'e_range'  : (k_edge-50, k_edge+90),
           'e_ticks'   : [k_edge-51, k_edge-3, k_edge+44, k_edge+91],
           'prop_cycle': [{'color'     : ['black', 'red', 'forestgreen'],
                           'linewidth' : [1.5    , 1.5  , 1.5          ],},
                          {'color'     : ['black', 'red', 'grey'],
                           'linewidth' : [1.5    , 1.5  , 1.5   ],}
                         ]}

fig, ax = fig_lcf(out, offset=offset, fig_pars=fig_pars, **fig_kws)
fig.tight_layout()
plt.show()
../../_images/tutorials_03.desmau_04.linear_comb_fitting_12_0.png

5. Performing EXAFS LCF

The lcf() function also allows LCF of the EXAFS spectrum by specifying the fit_region='exafs'. Note that here we additionally declare a dictionary for the background removal parameters, and modify fit_range to consider values in wavenumber space (\(k\)).

Once the LCF is finished we can print a summary with the lcf_report() function, and plot the resulting fit with the fig_lcf() function.

[7]:
# parameters for normalization, background removal and lcf
k_edge = 7112

autobk_kws   = {'k_range'   : [0, 15],
                'kweight'   : 2,
                'rbkg'      : 1,
                'win'       : 'hanning',
                'dk'        : 0.1,
                'clamp_hi'  : 35}

lcf_kws      = {'fit_region': 'exafs',
                'sum_one'   : False,
                'fit_range'  : [2, 12]}

collection.apply(autobk, **autobk_kws)
out = lcf(collection, **lcf_kws)
print(lcf_report(out))
[[Parameters]]
    fit_region         = exafs
    fit_range          = [2, 12]
    sum_one            = False
    kweight            = 2
[[Groups]]
    scan               = sample
    ref1               = Ferrihydrite_20K
    ref2               = Goethite_20K
[[Fit Statistics]]
    # fitting method   = leastsq
    # function evals   = 10
    # data points      = 201
    # variables        = 2
    chi-square         = 0.03903089
    reduced chi-square = 1.9614e-04
    Akaike info crit   = -1713.88806
    Bayesian info crit = -1707.28145
[[Variables]]
    amp1:  0.40386852 +/- 0.00361482 (0.90%) (init = 0.5)
    amp2:  0.59444931 +/- 0.00300275 (0.51%) (init = 0.5)
[[Correlations]] (unreported correlations are < 0.100)
    C(amp1, amp2) = -0.923
[8]:
# figure parameters
offset   = 2.0
fig_kws  = {'figsize'   : (8, 5)}  # size figure
fig_pars = {'prop_cycle': [{'color'     : ['black', 'red', 'forestgreen'],
                            'linewidth' : [1.5    , 1.5  , 1.5          ]},
                           {'color'     : ['black', 'red', 'grey'],
                            'linewidth' : [1.5    , 1.5  , 1.5   ]}
                          ]}

fig, ax = fig_lcf(out, offset=offset, fig_pars=fig_pars, **fig_kws)
fig.tight_layout()
plt.show()
../../_images/tutorials_03.desmau_04.linear_comb_fitting_15_0.png