Getting Started

Welcome to lmfitxps, a small Python package designed as an extension for the popular lmfit package, specifically tailored for X-ray Photoelectron Spectroscopy (XPS) data analysis.

While lmfit provides simple tools to build complex fitting models for non-linear least-squares problems and applies these models to real data, as well as introduces several built-in models, lmfitxps acts as an extension to lmfit designed for XPS data analysis. lmfitxps provides a comprehensive set of functions and models that facilitate the fitting of XPS spectra.

Although lmfit already provides several useful models for fitting XPS data, it often proves insufficient in adequately representing experimental XPS data out of the box. In the context of XPS experiments, the observed data is a convolution of both the sample’s underlying physical properties and a Gaussian component arising from experimental broadening.

This Gaussian distribution serves as an effective approximation for the convolution of three distinct Gaussian broadening functions, each of which contributes to the complex interplay inherent in the photoemission process:

  1. Broadening caused by the excitation source.

  2. Broadening resulting from thermal broadening and vibration modes (phonon broadening, depending on the material).

  3. Broadening introduced by the analyzer/spectrometer.

For further details, please refer to, for example, the Practical guide for curve fitting in x-ray photoelectron spectroscopy by G.H. Major et al.

lmfitxps therefore provides convolution functions based on scipy’s and numpy’s convolution functions to enable users to build custom lmfit CompositeModels using convolution of models. In addition, lmfitxps provides several pre-built models that use convolutions with model functions from lmfit and offer users the following options:

Table 1 Peak-like/Step-like Models

Model

Description

ConvGaussianDoniachSinglett

Convolution of a Gaussian with a Doniach lineshape used to fit singlet XPS peaks such as s-orbitals.

ConvGaussianDoniachDublett

Convolution of a Gaussian with a pair of Doniach lineshapes used to fit doublet XPS peaks such as p-, d-, f-orbitals.

FermiEdgeModel

Convolution of a Gaussian with a Fermi Dirac Step function using the thermal distribution lineshape from lmfit.

In addition to models for fitting signals in XPS data, lmfitxps introduces several background models that can be included in fit models instead of subtracting precalculated backgrounds. This is known as an active approach as suggested by A. Herrera-Gomez and generally leads to better fit results. The available background models are:

Table 2 Background Models

Model

Description

ShirleyBG

The commonly used step-like Shirley background.

TougaardBG

The Tougaard background based on four-parameter loss function (4-PIESCS) as suggested by R.Hesse.

SlopeBG | Calculates a sloping background

In addition to these discussed models, lmfitxps provides all underlying functions that serve as bases for these models. Furthermore, it includes functions for removing Tougaard and Shirley background components before performing data fitting.

Installation

Stable Version

To install the stable version of lmfitxps, simply use pip:

$ pip install lmfitxps

If required packages were not automatically installed during pip installation or are not yet present on your system, please install these requirements:

lmfit>=1.1.0
matplotlib>=3.6
numpy>=1.19
scipy>=1.6

Development Version

To install the development version or contribute to lmfitxps, please clone the GitHub repository:

$ git clone https://github.com/Julian-Hochhaus/lmfitxps.git

General Workflow for Predefined Models

Using one of the predefined models in lmfitxps typically follows this schematic approach:

import numpy as np
from lmfitxps.models import ChoosenModel

# Import your data, ensuring that energy (x) and intensity (y) values are stored in arrays
x = np.array([...])  # Replace with your energy data
y = np.array([...])  # Replace with your intensity data

# Initialize the model
model = ChoosenModel(prefix='choosen_model_')  # Model parameters will have the specified prefix

# Define initial parameters for the model
params = model.make_params(param1=10, param2=40)

# Fit the model to the data
result = model.fit(y, params, x=x)

# Access the fit results
print(result.fit_report())

The result object, which is an instance of the ModelResult class, contains several important properties:

  • `fit_report()`: Returns a printable fit report containing fit statistics and best-fit values along with uncertainties and correlations.

  • `best_fit`: The model function evaluated with best-fit parameters.

  • `residual`: Holds the residuals—the difference between observed data and fitted model.

  • `eval_components()`: Evaluates each component of a composite model function.

  • Fit Statistics: Various parameters indicating goodness-of-fit, such as Akaike Information Criterion (aic), Bayesian Information Criterion (bic), best-fit chi-square statistics (chisqr), and reduced chi-square (redchi).

For additional details about the ModelResult class and its methods and attributes, please refer to lmfit ModelResult documentation.

Usage Examples

FermiModel

fermibin

fermikin

To see source code, please expand:
import numpy as np
import matplotlib.pyplot as plt
import lmfit
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from lmfitxps import models
import matplotlib as mpl

exec_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
fermi = models.FermiEdgeModel(prefix='fermi_', independent_vars=["x"])
const = lmfit.models.ConstantModel(prefix='const_')
fit_model=fermi+const
data = np.genfromtxt(exec_dir + '/examples/FermiEdge.csv', delimiter=',', skip_header=1)
x = data[:, 0]
y = data[:, 1]
output_dir = os.path.join(exec_dir, 'examples/', 'plots')
os.makedirs(output_dir, exist_ok=True)

fig, (ax1, ax2) = plt.subplots(nrows=2,gridspec_kw={'height_ratios': [1, 1]}, sharex=True)
fig.patch.set_facecolor('#FCFCFC')
params = lmfit.Parameters()
params.add('fermi_amplitude', value=232)
params.add('fermi_sigma', value=0.2126)
params.add('fermi_kt', value=0.0026, vary=False)
params.add('fermi_center', value=236)
params.add('const_c', value=np.min(y))


result = fit_model.fit(y, params, y=y, x=x, weights=1 /(np.sqrt(y)))
comps = result.eval_components(x=x, y=y)
print(result.fit_report())

cmap = mpl.colormaps['tab20']
ax1.plot(x, result.best_fit, label='Best Fit', color=cmap(0))
ax1.plot(x, y, 'x', markersize=4, label='Data Points', color=cmap(2))

ax1.plot(x, comps['const_'], label='const. background', color='black')
ax1.plot(x, comps['fermi_'] + comps['const_'], color=cmap(4), label="fermi-edge")

ax1.fill_between(x, comps['fermi_'] + comps['const_'], comps['const_'], alpha=0.5,color=cmap(5))
ax1.legend()
ax1.set_xlabel('kin. energy in eV')
ax1.set_ylabel('intensity in arb. units')
# Set ticks only inside
ax1.tick_params(axis='x', which='both',top=True, direction='in')
ax1.tick_params(axis='y', which='both', right=True,direction='in')
ax2.tick_params(axis='x', which='both',top=True, direction='in')
ax2.tick_params(axis='y', which='both', right=True, direction='in')

ax1.set_yticklabels([])
ax1.set_title(f'FermiEdgeModel using kin. energy scale')
fig.subplots_adjust(hspace=0)
ax1.set_xlim(np.min(x), np.max(x))
# Residual plot
residual = result.residual
ax2.plot(x, residual)
ax2.set_xlabel('kin. energy in eV')
ax2.set_ylabel('Residual')

plot_filename = os.path.join(output_dir, f'plot_fermi_kin.png')
fig.savefig(plot_filename, dpi=300)
plt.close(fig)


fermi = models.FermiEdgeModel(prefix='fermi_', independent_vars=["x"])
const = lmfit.models.ConstantModel(prefix='const_')
fit_model=fermi+const
data = np.genfromtxt(exec_dir + '/examples/FermiEdge.csv', delimiter=',', skip_header=1)
x = 240-data[:, 0]
y = data[:, 1]
output_dir = os.path.join(exec_dir, 'examples/', 'plots')
os.makedirs(output_dir, exist_ok=True)

fig2, (ax21, ax22) = plt.subplots(nrows=2,gridspec_kw={'height_ratios': [1, 1]}, sharex=True)
fig2.patch.set_facecolor('#FCFCFC')
params = lmfit.Parameters()
params.add('fermi_amplitude', value=232, min=220, max=240)
params.add('fermi_sigma', value=0.15, min=0.12,max=0.17)
params.add('fermi_kt', value=0.0026, vary=False)
params.add('fermi_center', value=3.5, min=3, max=4)
params.add('const_c', value=np.min(y), min=np.min(y), max=np.mean(y))


result = fit_model.fit(y, params, y=y, x=x, weights=1 /(np.sqrt(y)))
comps = result.eval_components(x=x, y=y)
print(result.fit_report())

cmap = mpl.colormaps['tab20']
ax21.plot(x, result.best_fit, label='Best Fit', color=cmap(0))
ax21.plot(x, y, 'x', markersize=4, label='Data Points', color=cmap(2))

ax21.plot(x, comps['const_'], label='const. background', color='black')
ax21.plot(x, comps['fermi_'] + comps['const_'], color=cmap(4), label="fermi-edge")

ax21.fill_between(x, comps['fermi_'] + comps['const_'], comps['const_'], alpha=0.5,color=cmap(5))
ax21.legend()
ax21.set_xlabel('bin. energy in eV')
ax21.set_ylabel('intensity in arb. units')
# Set ticks only inside
ax21.tick_params(axis='x', which='both',top=True, direction='in')
ax21.tick_params(axis='y', which='both', right=True,direction='in')
ax22.tick_params(axis='x', which='both',top=True, direction='in')
ax22.tick_params(axis='y', which='both', right=True, direction='in')

ax21.set_yticklabels([])
ax21.set_title(f'FermiEdgeModel using bin. energy scale')
fig2.subplots_adjust(hspace=0)
ax21.set_xlim(np.min(x), np.max(x))
# Residual plot
residual = result.residual
ax22.plot(x, residual)
ax22.set_xlabel('bin. energy in eV')
ax22.set_ylabel('residual')
ax22.set_xlim(np.max(x), np.min(x))

plot_filename = os.path.join(output_dir, f'plot_fermi_bin.png')
fig2.savefig(plot_filename, dpi=300)
plt.close(fig2)

ConvGaussianDoniachSinglett with ShirleyBG Model

singlettbin

singlettkin

To see source code, please expand:
import numpy as np
import matplotlib.pyplot as plt
import lmfit
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from lmfitxps import models
import matplotlib as mpl

exec_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
singlett = models.ConvGaussianDoniachSinglett(prefix='singlett_', independent_vars=["x"])
bg=models.ShirleyBG(independent_vars=["y"], prefix='shirley_')
fit_model=singlett+bg
data = np.genfromtxt(exec_dir + '/examples/clean_Au_4f.csv', delimiter=',', skip_header=0)
x = data[150:, 0]
y = data[150:, 1]
output_dir = os.path.join(exec_dir, 'examples/', 'plots')
os.makedirs(output_dir, exist_ok=True)

fig, (ax1, ax2) = plt.subplots(nrows=2,gridspec_kw={'height_ratios': [1, 1]}, sharex=True)
fig.patch.set_facecolor('#FCFCFC')

params = lmfit.Parameters()
params.add('shirley_k', value=0.002)
params.add('shirley_const', value=100)
params.add('singlett_amplitude', value=np.max(y))
params.add('singlett_sigma', value=0.2126)
params.add('singlett_gamma', value=0.0, vary=False)
params.add('singlett_gaussian_sigma', value=0.0892)
params.add('singlett_center', value=92)

result = fit_model.fit(y, params, y=y, x=x, weights=1 /(np.sqrt(y)))
comps = result.eval_components(x=x, y=y)
print(result.fit_report())

cmap = mpl.colormaps['tab20']
ax1.plot(x, result.best_fit, label='Best Fit', color=cmap(0))
ax1.plot(x, y, 'x', markersize=4, label='Data Points', color=cmap(2))

ax1.plot(x, comps['shirley_'], label='Shirley background', color='black')
ax1.plot(x, comps['singlett_'] + comps['shirley_'], color=cmap(4), label="Doniach-Peak")

ax1.fill_between(x, comps['singlett_'] + comps['shirley_'], comps['shirley_'], alpha=0.5,color=cmap(5))
ax1.legend()
ax1.set_xlabel('bin. energy (eV)')
ax1.set_ylabel('intensity in arb. units')
# Set ticks only inside
ax1.tick_params(axis='x', which='both',top=True, direction='in')
ax1.tick_params(axis='y', which='both', right=True,direction='in')
ax2.tick_params(axis='x', which='both',top=True, direction='in')
ax2.tick_params(axis='y', which='both', right=True, direction='in')

ax1.set_yticklabels([])
ax1.set_title(f'ConvGaussianDoniachSinglett using kin. energy scale')
fig.subplots_adjust(hspace=0)
ax1.set_xlim(np.min(x), np.max(x))
# Residual plot
residual = result.residual
ax2.plot(x, residual)
ax2.set_xlabel('kin. energy in eV')
ax2.set_ylabel('Residual')

plot_filename = os.path.join(output_dir, f'plot_singlett_kin.png')
fig.savefig(plot_filename, dpi=300)
plt.close(fig)


singlett = models.ConvGaussianDoniachSinglett(prefix='singlett_', independent_vars=["x"])
bg=models.ShirleyBG(independent_vars=["y"], prefix='shirley_')
fit_model=singlett+bg
data = np.genfromtxt(exec_dir + '/examples/clean_Au_4f.csv', delimiter=',', skip_header=0)
x = 180-data[150:, 0]
y = data[150:, 1]
output_dir = os.path.join(exec_dir, 'examples/', 'plots')
os.makedirs(output_dir, exist_ok=True)

fig2, (ax21, ax22) = plt.subplots(nrows=2,gridspec_kw={'height_ratios': [1, 1]}, sharex=True)
fig2.patch.set_facecolor('#FCFCFC')

params = lmfit.Parameters()
params.add('shirley_k', value=0.002)
params.add('shirley_const', value=3000)
params.add('singlett_amplitude', value=np.max(y))
params.add('singlett_sigma', value=0.15)
params.add('singlett_gamma', value=0.0, vary=False)
params.add('singlett_gaussian_sigma', value=0.15)
params.add('singlett_center', value=87)


result = fit_model.fit(y, params, y=y, x=x, weights=1 /(np.sqrt(y)))
comps = result.eval_components(x=x, y=y)
print(result.fit_report())
cmap = mpl.colormaps['tab20']
ax21.plot(x, result.best_fit, label='Best Fit', color=cmap(0))
ax21.plot(x, y, 'x', markersize=4, label='Data Points', color=cmap(2))

ax21.plot(x, comps['shirley_'], label='Shirley background', color='black')
ax21.plot(x, comps['singlett_'] + comps['shirley_'], color=cmap(4), label="Doniach-Peak")

ax21.fill_between(x, comps['singlett_'] + comps['shirley_'], comps['shirley_'], alpha=0.5,color=cmap(5))
ax21.legend()
ax21.set_xlabel('bin. energy (eV)')
ax21.set_ylabel('intensity in arb. units')
# Set ticks only inside
ax21.tick_params(axis='x', which='both',top=True, direction='in')
ax21.tick_params(axis='y', which='both', right=True,direction='in')
ax22.tick_params(axis='x', which='both',top=True, direction='in')
ax22.tick_params(axis='y', which='both', right=True, direction='in')

ax21.set_yticklabels([])
ax21.set_title(f'ConvGaussianDoniachSinglett using bin. energy scale')
fig2.subplots_adjust(hspace=0)
ax21.set_xlim(np.min(x), np.max(x))
# Residual plot
residual = result.residual
ax22.plot(x, residual)
ax22.set_xlabel('bin. energy (eV)')
ax22.set_ylabel('residual')
ax22.set_xlim(np.max(x), np.min(x))
plot_filename = os.path.join(output_dir, f'plot_singlett_bin.png')
fig2.savefig(plot_filename, dpi=300)
plt.close(fig2)

ConvGaussianDoniachDublett with TougaardBG Model

dublettbin

dublettkin

To see source code, please expand:
import numpy as np
import matplotlib.pyplot as plt
import lmfit
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
from lmfitxps import models
import matplotlib as mpl

exec_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
dublett = models.ConvGaussianDoniachDublett(prefix='dublett_', independent_vars=["x"])
bg=models.TougaardBG(independent_vars=["x","y"], prefix='tougaard_')
fit_model=dublett+bg
data = np.genfromtxt(exec_dir + '/examples/clean_Au_4f.csv', delimiter=',', skip_header=0)
x = data[:, 0]
y = data[:, 1]
output_dir = os.path.join(exec_dir, 'examples/', 'plots')
os.makedirs(output_dir, exist_ok=True)

fig, (ax1, ax2) = plt.subplots(nrows=2,gridspec_kw={'height_ratios': [1, 1]}, sharex=True)
fig.patch.set_facecolor('#FCFCFC')

params = lmfit.Parameters()
params.add('tougaard_B', value=148.969)
params.add('tougaard_C', value=144.506, vary=False)
params.add('tougaard_D', value=268.598, vary=False)
params.add('tougaard_C_d', value=0.281, vary=False)
params.add('tougaard_extend', value=30)
params.add('dublett_amplitude', value=np.max(y))
params.add('dublett_sigma', value=0.2126)
params.add('dublett_gamma', value=0.04)
params.add('dublett_gaussian_sigma', value=0.0892)
params.add('dublett_center', value=92.2273)
params.add('dublett_soc', value=3.67127)
params.add('dublett_height_ratio', value=0.7)
params.add('dublett_fct_coster_kronig', value=1.04)

result = fit_model.fit(y, params, y=y, x=x, weights=1 /(np.sqrt(y)))
comps = result.eval_components(x=x, y=y)
print(result.fit_report())

cmap = mpl.colormaps['tab20']
ax1.plot(x, result.best_fit, label='Best Fit', color=cmap(0))
ax1.plot(x, y, 'x', markersize=4, label='Data Points', color=cmap(2))

ax1.plot(x, comps['tougaard_'], label='Tougaard background', color='black')
ax1.plot(x, comps['dublett_'] + comps['tougaard_'], color=cmap(4), label="Doniach-Dublett")

ax1.fill_between(x, comps['dublett_'] + comps['tougaard_'], comps['tougaard_'], alpha=0.5,color=cmap(5))
ax1.legend()
ax1.set_xlabel('bin. energy in eV')
ax1.set_ylabel('intensity in arb. units')
# Set ticks only inside
ax1.tick_params(axis='x', which='both',top=True, direction='in')
ax1.tick_params(axis='y', which='both', right=True,direction='in')
ax2.tick_params(axis='x', which='both',top=True, direction='in')
ax2.tick_params(axis='y', which='both', right=True, direction='in')

ax1.set_yticklabels([])
ax1.set_title(f'ConvGaussian DoniachDublett using kin. energy scale')
fig.subplots_adjust(hspace=0)
ax1.set_xlim(np.min(x), np.max(x))
# Residual plot
residual = result.residual
ax2.plot(x, residual)
ax2.set_xlabel('kin. energy in eV')
ax2.set_ylabel('Residual')

plot_filename = os.path.join(output_dir, f'plot_dublett_kin.png')
fig.savefig(plot_filename, dpi=300)
plt.close(fig)


dublett = models.ConvGaussianDoniachDublett(prefix='dublett_', independent_vars=["x"])
bg=models.TougaardBG(independent_vars=["x","y"], prefix='tougaard_')
fit_model=dublett+bg
data = np.genfromtxt(exec_dir + '/examples/clean_Au_4f.csv', delimiter=',', skip_header=0)
x = 180-data[:, 0]
y = data[:, 1]
output_dir = os.path.join(exec_dir, 'examples/', 'plots')
os.makedirs(output_dir, exist_ok=True)

fig2, (ax21, ax22) = plt.subplots(nrows=2,gridspec_kw={'height_ratios': [1, 1]}, sharex=True)
fig2.patch.set_facecolor('#FCFCFC')

params = lmfit.Parameters()
params.add('tougaard_B', value=148.969)
params.add('tougaard_C', value=144.506, vary=False)
params.add('tougaard_D', value=268.598, vary=False)
params.add('tougaard_C_d', value=0.281, vary=False)
params.add('tougaard_extend', value=30)
params.add('dublett_amplitude', value=np.max(y))
params.add('dublett_sigma', value=0.2126)
params.add('dublett_gamma', value=0.04, min=0)
params.add('dublett_gaussian_sigma', value=0.0892)
params.add('dublett_center', value=87.663)
params.add('dublett_soc', value=3.67127)
params.add('dublett_height_ratio', value=0.7)
params.add('dublett_fct_coster_kronig', value=1.04)



result = fit_model.fit(y, params, y=y, x=x, weights=1 /(np.sqrt(y)))
comps = result.eval_components(x=x, y=y)
print(result.fit_report())
cmap = mpl.colormaps['tab20']
ax21.plot(x, result.best_fit, label='Best Fit', color=cmap(0))
ax21.plot(x, y, 'x', markersize=4, label='Data Points', color=cmap(2))

ax21.plot(x, comps['tougaard_'], label='Tougaard background', color='black')
ax21.plot(x, comps['dublett_'] + comps['tougaard_'], color=cmap(4), label="Doniach-Dublett")

ax21.fill_between(x, comps['dublett_'] + comps['tougaard_'], comps['tougaard_'], alpha=0.5,color=cmap(5))
ax21.legend()
ax21.set_xlabel('bin. energy in eV')
ax21.set_ylabel('intensity in arb. units')
# Set ticks only inside
ax21.tick_params(axis='x', which='both',top=True, direction='in')
ax21.tick_params(axis='y', which='both', right=True,direction='in')
ax22.tick_params(axis='x', which='both',top=True, direction='in')
ax22.tick_params(axis='y', which='both', right=True, direction='in')

ax21.set_yticklabels([])
ax21.set_title(f'ConvDoniachDublettModel using bin. energy scale')
fig2.subplots_adjust(hspace=0)
ax21.set_xlim(np.min(x), np.max(x))
# Residual plot
residual = result.residual
ax22.plot(x, residual)
ax22.set_xlabel('bin. energy in eV')
ax22.set_ylabel('residual')
ax22.set_xlim(np.max(x), np.min(x))
plot_filename = os.path.join(output_dir, f'plot_dublett_bin.png')
fig2.savefig(plot_filename, dpi=300)
plt.close(fig2)
  • In all cases , it can be observed that fits for binding energy and kinetic energy align well.

  • The selected models do not perfectly match all data; in practice , a second component would likely be necessary to achieve optimal fitting results . Here , these models are used purely as examples.

  • Due to how fitting procedures operate , small differences between fits for binding energy and kinetic energy scales are expected (most easily seen in residuals). This is a natural consequence of fitting processes where only local minima may be reached.