The Adaptive Gain Integrating Pixel Detector is a high speed detector intended for use at the European XFEL. It allows single pulse imaging at 4.5 MHz frame rate with a dynamic range allowing single photon detection and detection of more than 10.000 12.4 keV photons in the same image.
The AGIPD camera consists of four individually moveable quadrants, each having four detector tiles with 512 × 128 pixels per tile, giving a total of 1024 × 1024, or roughly 1 million pixels. https://doi.org/10.1107/S1600577518016077
In this tutorial exposes a way to fit the gaps of the the 4 quadrants. Data courtesy of Johannes Möller, Desy/EuXFEL.
Considering the displacement of the modules...
%matplotlib notebook
import os
import numpy
from matplotlib.pyplot import subplots
import pyFAI
from pyFAI.azimuthalIntegrator import AzimuthalIntegrator
from pyFAI.distortion import Distortion
from pyFAI.gui import jupyter
import time
t0 = time.time()
#I got some better data from my colleagues. It's silicon titanate measured at
#10.235 keV.
#Spatial distortion calculated:
spatial_distortion = numpy.load('AGIPDdistortion.npy')
print(spatial_distortion.dtype, spatial_distortion.shape)
#setup pyFAI azimuthal integrator with spatial distortion for AGIPD1M.
agipd = pyFAI.detectors.Detector(200e-6, 200e-6)
agipd.aliases = ["AGIPD1M"]
agipd.shape = (1024,1024)
agipd.mask = numpy.zeros((1024,1024))
agipd.set_pixel_corners(spatial_distortion)
ai = AzimuthalIntegrator(detector=agipd)
ai.setFit2D(5500,621,649)
ai.wavelength = 1.33e-10
#Display the module position in space
fig, ax = subplots()
ax.scatter(spatial_distortion[:,:,1,2],spatial_distortion[:,:,1,1],marker=",")
ax.set_title('spatial distortion')
saxs_data = numpy.load('agipd_saxs.npy')
jupyter.display(saxs_data)
agipd.distortion_correction = Distortion(agipd,resize=True)
plotdata = agipd.distortion_correction.correct(saxs_data)
jupyter.display(plotdata)
# Save the detector definition
dest = "agipd.h5"
if os.path.exists(dest):
os.unlink(dest)
agipd.save(dest)
powder = numpy.load("agipd_powder.npy")
jupyter.display(powder.clip(0,500))
det = pyFAI.detector_factory("agipd.h5")
abs(det.get_pixel_corners()-agipd.get_pixel_corners()).max()
cor = Distortion(detector=det, resize = True)
powder2 = powder.copy()
powder2[numpy.logical_not(numpy.isfinite(powder))] = 0
powder2[powder<0] = 0
dis = cor.correct(powder2)
jupyter.display(cor.correct(powder2).clip(0, 500))
mask = numpy.logical_not(numpy.logical_and(numpy.isfinite(powder), powder>0))
maska = numpy.ones(agipd.shape)
maska[:512, :512] = 0
QA = numpy.logical_or(mask, maska)
agipd.mask = QA
agipd.save('agipd_A.h5')
jupyter.display(QA, label="Quadrant A")
maska = numpy.ones(agipd.shape)
maska[:512, 512:] = 0
QA = numpy.logical_or(mask, maska)
agipd.mask = QA
agipd.save('agipd_B.h5')
jupyter.display(QA, label="Quadrant B")
maska = numpy.ones(agipd.shape)
maska[512:, 512:] = 0
QA = numpy.logical_or(mask, maska)
agipd.mask = QA
agipd.save('agipd_C.h5')
jupyter.display(QA, label="Quadrant C")
maska = numpy.ones(agipd.shape)
maska[512:, :512] = 0
QA = numpy.logical_or(mask, maska)
agipd.mask = QA
agipd.save('agipd_D.h5')
jupyter.display(QA, label="Quadrant D")
Each quadrant is calibrated separately. The PONI and the control points are saved accordingly:
from pyFAI import goniometer
from pyFAI.control_points import ControlPoints
class AgipdTrans(goniometer.BaseTransformation):
def __init__(self):
goniometer.BaseTransformation.__init__(self, None, param_names = ["dist", "rot1", "rot2", "rot3",
"A1", "A2", "B1", "B2",
"C1", "C2", "D1", "D2"],)
self.codes = {}
def __call__(self,param, pos):
"Actually pos is the quadrant label"
return goniometer.PoniParam(param[0],
param[(ord(pos)-65)*2+4],
param[(ord(pos)-65)*2+5],
param[1],
param[2],
param[3])
agipd_trans = AgipdTrans()
cal = pyFAI.calibrant.Calibrant("Li4Ti5O12.D")
wavelength=1.21137466924e-10
cal.wavelength=wavelength
ai_A = pyFAI.load("cas_A.poni")
ai_B = pyFAI.load("cas_B.poni")
ai_C = pyFAI.load("cas_C.poni")
ai_D = pyFAI.load("cas_D.poni")
cp_A = ControlPoints("cas_A.npt")
cp_B = ControlPoints("cas_B.npt")
cp_C = ControlPoints("cas_C.npt")
cp_D = ControlPoints("cas_D.npt")
meta_detector = goniometer.GoniometerRefinement([ai_A.dist, 0, 0, 0,
ai_A.poni1, ai_A.poni2,
ai_B.poni1, ai_B.poni2,
ai_C.poni1, ai_C.poni2,
ai_D.poni1, ai_D.poni2],
trans_function=agipd_trans,
pos_function=(lambda a:a),
detector = "agipd.h5",
wavelength=wavelength
)
meta_detector.new_geometry("A", powder, "A", cp_A, cal, ai_A)
meta_detector.new_geometry("B", powder, "B", cp_B, cal, ai_B)
meta_detector.new_geometry("C", powder, "C", cp_C, cal, ai_C)
meta_detector.new_geometry("D", powder, "D", cp_D, cal, ai_D)
meta_detector.residu2(meta_detector.param)
#meta_detector.single_geometries["A"].get_position()
meta_detector.trans_function(meta_detector.param, "D")
res = meta_detector.refine2()
#Reconstruction of the detector
det = pyFAI.detector_factory("agipd.h5")
new_distortion = det._pixel_corners[...]
A_min1 = new_distortion[:512, :512,:,1].min()
A_min2 = new_distortion[:512, :512,:,2].min()
#A
new_distortion[:512, :512,:,1] -= A_min1
new_distortion[:512, :512,:,2] -= A_min2
#B
new_distortion[:512, 512:,:,1] += res[4] - res[6] - A_min1
new_distortion[:512, 512:,:,2] += res[5] - res[7] - A_min2
#C
new_distortion[512:, 512:,:,1] += res[4] - res[8] - A_min1
new_distortion[512:, 512:,:,2] += res[5] - res[9] - A_min2
#D
new_distortion[512:, :512,:,1] += res[4] - res[10] - A_min1
new_distortion[512:, :512,:,2] += res[5] - res[11] - A_min2
import copy
agipd2 = copy.copy(det)
agipd2.set_pixel_corners(new_distortion)
agipd2.save("agipd_fitted.h5")
cor2 = Distortion(detector=agipd2, resize=True)
jupyter.display(cor2.correct(powder2).clip(0,1000))
This tutorial explains how to fit multiple sub-detector together and to combine them together into a single one.
print("Total execution time:", time.time()-t0)