X-ray reflectometry reduction in Python¶
islatu
is an open-source pacakge for the reduction of x-ray reflectometry datasets.
Currently, islatu
is developed at and supports data from Diamond Light Source, however we are happy to work with others to enable data from other sources (including neutron sources).
These webpages include API-level documentation and information about some workflows that can be used for data reduction. There is also documentation on a command line interface that can be used to process reflectivity data without any python programming.
Contributing¶
As with any coding project, there are many ways to contribue. To report a bug or suggest a feature, open an issue on the github repository. If you would like to contribute code, we would recommend that you first raise an issue before diving into writing code, so we can let you know if we are working on something similar already. To e.g. fix typos in documentation or in the code, or for other minor changes, feel free to make pull requests directly.
Contact us¶
If you need to contact the developers about anything, please either raise an issue on the github repository if appropriate, or send an email to richard.brearton@diamond.ac.uk.
Contributors¶
Acknowledgements¶
We acknowledge the support of the Ada Lovelace Centre – a joint initiative between the Science and Technology Facilities Council (as part of UK Research and Innovation), Diamond Light Source, and the UK Atomic Energy Authority, in the development of this software.
Installation¶
islatu
can be installed from the PyPI package manager with pip
:
pip install islatu
Alternatively, the latest development build can be found Github.
Reduction workflows¶
A typical data reduction workflow handled by islatu
is shown here:
I07 reflectometry data¶
Detailed here are the techniques one can use to reduce reflectometry data acquired on the I07 beamline at diamond using the islatu package. Both the vertical scattering and the double-crystal deflector geometries are discussed. The particular example of reduction of reflectometry data collected from a water-air interface is worked through in detail.
At I07, a reflectometry profile is generally constructed from multiple scans. Each scan has a corresponding nexus file (.nxs) which contains metadata, and an hdf5 file (.h5) containing the images recorded in the scan.
The first task is to instantiate an instance of islatu’s Profile class containing the reflectivity profile’s data. To do this, we need to download the profile’s constituent .h5 and .nxs files, and tell islatu where to find the .nxs files. Provided that your .h5 files are in the same directory as your .nxs files, islatu will be able to load your data.
[45]:
from islatu.refl_profile import Profile
from islatu.io import i07_nxs_parser
# On my machine, I have some .nxs and .h5 files in this directory.
data_dir = "../../tests/resources/"
# I have scans 404875-404882 stored in the above directory.
scan_numbers = range(404875, 404882 + 1)
data_files = [f"{data_dir}i07-{i}.nxs" for i in scan_numbers]
# Profile.fromfilenames() expects a list of paths to data files, and a parser
# from islatu.io that it can use to parse the data files.
my_profile = Profile.fromfilenames(data_files, i07_nxs_parser)
Data file found at ../../tests/resources/excaliburScan404875_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404875_000001.h5
Currently loaded 51 images.
Data file found at ../../tests/resources/excaliburScan404876_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404876_000001.h5
Currently loaded 8 images.
Data file found at ../../tests/resources/excaliburScan404877_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404877_000001.h5
Currently loaded 8 images.
Data file found at ../../tests/resources/excaliburScan404878_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404878_000001.h5
Currently loaded 9 images.
Data file found at ../../tests/resources/excaliburScan404879_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404879_000001.h5
Currently loaded 13 images.
Data file found at ../../tests/resources/excaliburScan404880_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404880_000001.h5
Currently loaded 14 images.
Data file found at ../../tests/resources/excaliburScan404881_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404881_000001.h5
Currently loaded 31 images.
Data file found at ../../tests/resources/excaliburScan404882_000001.h5.
Loading images from file ../../tests/resources/excaliburScan404882_000001.h5
Currently loaded 26 images.
When we used the fromfilenames class method above, islatu loaded all of the images taken in the scan. It’s now possible to use the profile to get a very crude indication of what our data looks like. This is possible because islatu uses the default signal and default axis specified in the nexus file to guess a profile’s reflectivity as a function of Q or θ.
[46]:
# Note that any instantiated islatu profile has the following properties:
print(f"Theta values: {my_profile.theta}")
print(f"Corresponding q-vectors: {my_profile.q_vectors}")
print(f"Reflectivity: {my_profile.reflectivity}")
print(f"Errors on the reflectivity: {my_profile.reflectivity_e}")
Theta values: [0.045225 0.047505 0.049755 ... 3.525275 3.570413 3.615548]
Corresponding q-vectors: [0.01 0.010504 0.011002 ... 0.779022 0.788984 0.798945]
Reflectivity: [0.793168 0.80557 0.817332 ... 0.040644 0.040322 0.039764]
Errors on the reflectivity: [0.042844 0.043178 0.043492 ... 0.009698 0.00966 0.009593]
Let’s plot this!
[47]:
import plotly.graph_objects as go
# The following two lines are just required for the generation of this document.
import plotly
from plotly.offline import iplot
plotly.offline.init_notebook_mode()
def plot_xrr_curve(islatu_profile: Profile, title: str, log=True):
"""
Convenience function for plotting simple XRR curves.
"""
fig = go.Figure().update_layout(title=title,
xaxis_title='Q/Å', yaxis_title='R')
fig.add_trace(go.Scatter(x=(islatu_profile.q_vectors),
y=(islatu_profile.reflectivity), error_y={
"type": 'data',
"array": (islatu_profile.reflectivity_e),
"visible": True},
name="Islatu"))
if log:
fig.update_yaxes(type="log")
iplot(fig)
[48]:
title = "Uncorrected profile"
plot_xrr_curve(my_profile, title)
The above curve is pretty much nonsense, the sole exception being that the x-axis is correct. Before trying to perform any corrections, it is worth understanding more about the profile object that islatu has created for us.
Firstly, just as the real experiment was made up from a series of scans, so is the islatu profile object.
[49]:
print(my_profile.scans)
[<islatu.scan.Scan2D object at 0x137fc7e50>, <islatu.scan.Scan2D object at 0x147767820>, <islatu.scan.Scan2D object at 0x137fde1f0>, <islatu.scan.Scan2D object at 0x147562d30>, <islatu.scan.Scan2D object at 0x147838be0>, <islatu.scan.Scan2D object at 0x147510430>, <islatu.scan.Scan2D object at 0x1472aedc0>, <islatu.scan.Scan2D object at 0x147734e50>]
Each of these scans has its own q_vectors, reflectivities, and reflectivities_e, just as the overall profile does.
[50]:
first_scan = my_profile.scans[0]
print(f"Theta values: {first_scan.theta}")
print(f"Corresponding q-vectors: {first_scan.q_vectors}")
print(f"Reflectivity: {first_scan.reflectivity}")
print(f"Errors on the reflectivity: {first_scan.reflectivity_e}")
Theta values: [0.045225 0.047505 0.049755 ... 0.15377 0.156033 0.158291]
Corresponding q-vectors: [0.01 0.010504 0.011002 ... 0.034002 0.034502 0.035002]
Reflectivity: [0.940042 0.95474 0.96868 ... 0.018563 0.017533 0.016332]
Errors on the reflectivity: [0.050778 0.051173 0.051545 ... 0.007135 0.006935 0.006693]
Scans also have an attribute called metadata. This contains all of the metadata stored in that scan’s .nxs file. Islatu uses this object to expose several useful pieces of information, such as background and signal regions of interest, the distance between the detector and the sample, and much more (shown below).
[51]:
example_metadata = first_scan.metadata
print([attr for attr in dir(example_metadata) if not attr.startswith('_')])
['background_regions', 'default_axis', 'default_axis_name', 'default_axis_type', 'default_nxdata', 'default_nxdata_name', 'default_signal', 'default_signal_name', 'detector', 'detector_distance', 'detector_name', 'entry', 'excalibur_detector', 'instrument', 'local_data_path', 'local_path', 'nxfile', 'probe_energy', 'signal_regions', 'src_path', 'transmission']
Each of the scans in a profile contain detector images. Let’s visualize the first image taken in the first scan.
[52]:
# Image data is stored as a numpy array, so we'll want to load numpy to
# manipulate the data.
import numpy as np
first_image = first_scan.images[0]
# Because of how the heatmap will be displayed, to make x pixels lie along the
# x-axis we need to take the transpose of our data.
array_transpose = np.transpose(first_image.array)
# Let's also take the log of our data to make it easier to visualize.
array_log_transpose = np.log(array_transpose+0.1)
[53]:
# We'll want a simple function for plotting heatmaps.
def plot_heatmap(array_to_plot, title):
"""
Simple function that we can use to plot heatmaps of detector frames.
"""
fig = go.Figure().update_layout(title=title,
xaxis_title='x-pixels',
yaxis_title='y-pixels')
fig.add_trace(go.Heatmap(z=array_to_plot, colorscale='Jet'))
iplot(fig)
[54]:
# Now plot the image's array.
plot_heatmap(array_log_transpose, "Uncropped detector frame")