Skip to content

XAS and XMCD/XMLD Analysis

mmg_toolbox contains some useful tools for analysis of XAS spectra, in particular for calculation of subtracted polarised spectra for calculation of dichroic signals like XMCD and XMLD.

Introduction to the Spectra object

X-Ray Absorption Spectroscopy (XAS) spectra can be loaded as a pythonic object with special behaviours and attributes.

  • A Spectra object contains an energy array and a signal array. For example this could represent the TEY spectra from a scan.
  • Multiple Spectra can be contained within a SpectraContainer - for example different detectors for a single scan. SpectraContainers also contain metadata for a scan.
  • Spectra and SpectraContainers contain methods to perform background subtractions and other processing.
  • Spectra and SpectraContainers can be combined (averaged) and subtracted.
  • Both objects contain a history of all processes performed on them.

Spectra Data model

spectra = Spectra(energy, signal, mode, process)
metadata = XasMetadata(scan_no=1234, default_mode='tey', sample_name='Fe')
scan = SpectraContainer('name', {'mode': spectra}, metadata=metadata)
scan2 = scan + 2  # add 2 to signal of each contained mode
scan.remove_background()  # apply operation to each contained mode, store previous version in scan.parents

Spectra & SpectraContainer

Selected Behaviours (see Docs for full list)

command
print(spectra1) displays contained spectra, metadata and previous analysis steps
spectra1 + spectra2 Averages contained spectra on a regular energy grid
spectra1 - spectra2 Subtracts spectra on an interpolated energy grid
spectra1.trim(ev_from_start=1) trim contained spectra by 1 eV
spectra1.divide_by_preedge() divide contained spectra by preedge signal
spectra1.remove_background(type) Subtract background using various methods
spectra1.analysis_steps_str() returns a formatted string of previous analysis steps
spectra1.create_background_figure() create a matplotlib figure of all contained spectra
spectra1.create_background_figure() create a matplotlib figure including background subtraction
spectra1.write_nexus('filename.nxs') write a processed NeXus file

spectra.remove_background() options

Option parameters
'flat' ev_from_start
'norm' ev_from_start
'linear' ev_from_start
'curve' ev_from_start
'exp' ev_from_start, ev_from_end
'step' ev_from_start
'double_edge_step' l3_energy, l2_energy, peak_width_ev
'poly_edges' *step_energies, peak_width_ev
'exp_edges' *step_energies, peak_width_ev

Example Script - single XAS scan

from mmg_toolbox import xas
import matplotlib.pyplot as plt

# Load from NeXus file
spectra, = xas.load_xas_scans('12345.nxs', sample_name='mysample', mode='all', dls_loader=True)  # loads a SpectraContainer containing all Spectra in scan
print(spectra)

# SpectraContainer documentation
help(spectra)

# Plot Raw spectra
spectra.create_figure(figsize=[12, 4], dpi=60)

# Process spectra
spectra = spectra.divide_by_preedge()
spectra = spectra.remove_background('linear')  # options listed below

# list all processes performed, including any fitting parameters
print(spectra)

# Plot background subtracted spectra
spectra.create_background_figure(figsize=[12, 6], dpi=60)
plt.show()

Example Script - combining spectra

from mmg_toolbox import xas
import matplotlib.pyplot as plt

spectra1, spectra2 = xas.load_xas_scans('12345.nxs', '123456.nxs',
                                        sample_name='mysample', mode='all', dls_loader=True)
# normalise
spectra1 = spectra1.divide_by_preedge()
spectra2 = spectra2.divide_by_preedge()

# Average the spectra
average = spectra1 + spectra2
print(average)

average.create_figure(figsize=[12, 4], dpi=60)

# Subtract the spectra
diff = spectra1 - spectra2
print(diff)  # Note that for subtractions the sum rules are automatically calculated using the edge element

diff.create_figure(figsize=[12, 4], dpi=60)

print(diff.sum_rules_report(n_holes=4, mode='tey'))

diff.create_sum_rules_figure()
plt.show()

Example Script - Loading Spectra from an experiment

from mmg_toolbox import Experiment, xas
import matplotlib.pyplot as plt

# Create experiment object - monitors one or more data folders for files
exp = Experiment('{{experiment_dir}}', instrument='{{beamline}}')

print(exp)

# print all scans in directory (could take a while...)
print(exp.all_scans_str())

# Load scan data and plot raw spectra
scan_numbers = [12345, 12346, 12347, 12348]
scans = exp.scans(*scan_numbers)  # loads Scan objects that can access NeXus data file
spectras = exp.load_xas(*scan_numbers, sample_name='mysample', mode='tey', dls_loader=True)  # only loads NXxas spectra (energy scans) and creates a Spectra object

for scan in spectras:
    print(scan)

# Plot each RAW spectra
n_spectra = len(spectras[0].spectra)

fig, axes = plt.subplots(1, n_spectra, figsize=[6 * n_spectra, 6], dpi=100, squeeze=False)
for scan in spectras:
    for ax, (mode, spectra) in zip(axes.flat, scan.spectra.items()):
        spectra.plot(ax)
        ax.set_ylabel(f"{mode} / monitor")
        ax.set_xlabel('E [eV]')
        ax.legend()

fig.tight_layout()

# process the spectra
spectras = [spectra.divide_by_preedge().remove_background('linear') for spectra in spectras]

fig, axes = plt.subplots(1, n_spectra, figsize=[6 * n_spectra, 6], dpi=100, squeeze=False)
for scan in spectras:
    for ax, (mode, spectra) in zip(axes.flat, scan.spectra.items()):
        spectra.plot(ax)
        ax.set_title(spectra.process_label)
        ax.set_ylabel(mode)
        ax.set_xlabel('E [eV]')
        ax.legend()
fig.tight_layout()

# Average polarised scans
for xas_scan in spectras:
    print(f"{xas_scan.name}: {xas_scan.metadata.pol}")
pol1, pol2 = xas.average_polarised_scans(*spectras)
print(pol1)
print(pol2)

if pol2 is None:
    raise  ValueError(f"No opposite polarisations found: {[s.metadata.pol for s in spectras]}")

# Plot averaged scans
fig, axes = plt.subplots(1, n_spectra, figsize=[6 * n_spectra, 6], dpi=100, squeeze=False)
for scan in [pol1, pol2]:
    for ax, (mode, spectra) in zip(axes.flat, scan.spectra.items()):
        spectra.plot(ax)
        ax.set_title(spectra.process_label)
        ax.set_ylabel(mode)
        ax.set_xlabel('E [eV]')
        ax.legend()

# Calculate XMCD
xmcd = pol1 - pol2
print(xmcd)

xmcd.create_sum_rules_figure(figsize=(8 * n_spectra, 6), dpi=100);
plt.tight_layout(h_pad=0.1, w_pad=0.1)

# fig, axes = plt.subplots(1, n_spectra, figsize=[6 * n_spectra, 6], dpi=100, squeeze=False)
# for ax, (mode, spectra) in zip(axes.flat, xmcd.spectra.items()):
#     spectra.plot(ax)
#     ax.set_title(spectra.process_label)
#     ax.set_ylabel(mode)
#     ax.set_xlabel('E [eV]')
#     ax.legend()

# Create output file
# create processed nexus file
xmcd_filename = f"{spectras[0].metadata.scan_no}-{spectras[-1].metadata.scan_no}_{xmcd.name}.nxs"
xmcd.write_nexus(xmcd_filename)

plt.show()