Powder diffraction with spotty data … how to handle the spottiness.#

The data used for this tutorial are available on http://www.silx.org/pub/pyFAI/pyFAI_UM_2026/spotty/, it is a standard material SRM640, Silicon, recorded on the ID11 beamline with a sub-micron beam at 40 keV, using an Eiger2 CdTe detector at 200Hz. The Eiger file is the first data-file from a dataset containing 90 of them.

[1]:
%matplotlib inline
[2]:
import copy
import os
import glob
import time
import numpy
from matplotlib.pyplot import subplots
import fabio
from silx.resources import ExternalResources
import pyFAI
from pyFAI.gui import jupyter

start_time = time.perf_counter()
print(f"Using pyFAI version {pyFAI.version}")
Using pyFAI version 2026.1.0-dev0
[3]:
resource = ExternalResources("spotty", "http://www.silx.org/pub/pyFAI/pyFAI_UM_2026/spotty/")

Display the first and the average frame#

[4]:
fimg = fabio.open(resource.getfile("eiger_0000.h5"))
frame0 = fimg.data
fimg.nframes
[4]:
200
[5]:
# display the image:
jupyter.display(frame0);
../../_images/usage_tutorial_Spotty_6_0.png
[6]:
%%time
# Average the dataset:
num = numpy.zeros(frame0.shape, dtype=numpy.int64)  # Contains the sum of all signal
den = numpy.zeros(frame0.shape, dtype=numpy.int64)  # Contains the number of contributing pixels
for frame in fimg:
    num +=  numpy.where(frame.data<65535, frame.data, 0)
    den +=  numpy.where(frame.data<65535, 1, 0)
avg = numpy.zeros(frame0.shape, dtype=numpy.float64)
numpy.divide(num, den, where=den!=0, out=avg);
CPU times: user 4.95 s, sys: 692 ms, total: 5.64 s
Wall time: 5.66 s
[7]:
jupyter.display(avg);
../../_images/usage_tutorial_Spotty_8_0.png

Perform the azimuthal integration and compare with sigma-clipping and median filtering#

First on needs to load the azimuthal integrator.

[8]:
ai = pyFAI.load(resource.getfile("ID11_40keV.poni"))
ai
[8]:
Detector Eiger2 CdTe 4M  PixelSize= 75µm, 75µm   BottomRight (3)         CdTe,750µm
Wavelength= 0.305501 Å  Parallax: off
SampleDetDist= 1.178702e-01 m   PONI= 8.783056e-02, 7.342441e-02 m      rot1=-0.005004  rot2=0.001866  rot3=0.000000 rad
DirectBeamDist= 117.872 mm      Center: x=986.856, y=1174.007 pix       Tilt= 0.306° tiltPlanRotation= 20.455° λ= 0.306Å
[9]:
# Common integration parameters:
kwargs = {"npt": 2000,
          "unit": "2th_deg",
          "method": ("no", "csr", "cython"),
          "dummy": 65535,
         }
[10]:
res_aver = ai.integrate1d(avg, **kwargs)
res_sigma = ai.sigma_clip_ng(avg, **kwargs, error_model="azimuthal")
res_median = ai.medfilt1d_ng(avg, **kwargs)
[11]:
ax = jupyter.plot1d(res_aver, label="Average")
ax.plot(*res_sigma[:2], "--" ,label="Sigma-clip")
ax.plot(*res_median[:2], "--" ,label="Median")
ax.legend();
../../_images/usage_tutorial_Spotty_13_0.png

Calculate the spottiness for the first and the average frame:#

[12]:
kwargs["error_model"] = "azimuthal"
res_avg = ai.integrate1d(avg,    **kwargs)
res_fr0 = ai.integrate1d(frame0, **kwargs)
res_avg.calc_spottiness()
[12]:
0.025335838760482423
[13]:
print(f"Unweighted spottiness index: S(frame0)={res_fr0.calc_spottiness():.3f};\tS(avg)={res_avg.calc_spottiness():.3f}.")
print(f"I-Weighted spottiness index: S(frame0)={res_fr0.calc_spottiness(True):.3f};\tS(avg)={res_avg.calc_spottiness(True):.3f}.")
Unweighted spottiness index: S(frame0)=0.156;   S(avg)=0.025.
I-Weighted spottiness index: S(frame0)=0.209;   S(avg)=0.061.

Integrate all stack and plot spottiness for each frame in the stack#

[14]:
%%time
res = [ai.integrate1d(frame.data, **kwargs) for frame in fimg]
CPU times: user 2min 57s, sys: 206 ms, total: 2min 57s
Wall time: 8.01 s
[15]:
fig,ax = subplots()
ax.plot([r.calc_spottiness() for r in res], label="unweighted")
ax.plot([r.calc_spottiness(weighted=True) for r in res], label="I-Wighted")
ax.set_xlabel("Frame index")
ax.set_ylabel("Spottiness index")
ax.legend();
../../_images/usage_tutorial_Spotty_19_0.png

Accumulate frames to assess the evolution of spottiness with the number of frames#

This should evolve as \(1/\sqrt{N}\)

[16]:
# Accumulate 2 frames:
print("First", res[0].calc_spottiness())
print("Second", res[1].calc_spottiness())
print("1 + 2", res[1].union(res[0]).calc_spottiness())
First 0.15621348906463955
Second 0.16844202699901842
1 + 2 0.14151299563643194
[17]:
%%time
tmp = copy.deepcopy(res[0])
acc_u = [res[0].calc_spottiness()]
acc_w = [res[0].calc_spottiness(True)]
for i in res[1:]:
    tmp = i.union(tmp)
    acc_u.append(tmp.calc_spottiness())
    acc_w.append(tmp.calc_spottiness(True))
CPU times: user 89.8 ms, sys: 0 ns, total: 89.8 ms
Wall time: 89.8 ms
[18]:
fig,ax = subplots()
ax.plot(acc_u, label="Unweighted")
ax.plot(acc_w, label="I-Weighted")
ax.plot(acc_w[0]/numpy.sqrt(1+numpy.arange(fimg.nframes)), "--", label="1/√n")
ax.set_ylabel("Spottiness index")
ax.set_xlabel("Number of frames")
ax.set_title("Accumulated")
ax.legend();
../../_images/usage_tutorial_Spotty_23_0.png
[19]:
fig,ax = subplots(2)
jupyter.plot1d(res_aver, label="Integrated averaged frame", ax=ax[0])
ax[0].plot(tmp.radial,tmp.intensity,"1", label="Accumulated results")
ax[0].set_title("Average before integration vs accumulate integrated results")
ax[0].legend()
ax[1].plot(tmp.radial,res_aver.intensity - tmp.intensity, label="delta")
ax[1].set_xlabel("Scattering angle")
ax[1].set_ylabel("Delta");
# ax[1].legend();
../../_images/usage_tutorial_Spotty_24_0.png

Conclusions:#

  • Merging data, especially when they come from a Dectris detector, requires careful handling of the dynamically masked pixels

  • Azimuthal integration and frame averaging can be interchanged, but only under certain conditions

  • It is possible to separate Bragg spots from powder signal using sigma-clipping or median filtering in azimuthal space

  • The spottiness of the integrated pattern can be measured if the “azimuthal” error model was used.

  • Residual spottiness evolves as \(1/\sqrt{N}\)

[20]:
print(f"Run-time: {time.perf_counter()-start_time:.3f}s")
Run-time: 20.163s
[ ]: