Calibration of a very large Pilatus detector with overlapping grid position#
This tutorial presents the calibration of the Pilatus 900kw CdTe which is a very large 2D detector (4500x200) running at ESRF ID06-LVP . The detector is so large that the grid needs to be displaced in front of the detector and the operation needs to be performed several times.
The overall strategy is very similar to the ID15 detector calibration, except that all needs to be done 3 times, one for each of the grid position: left, center and right:
Image preprocessing
Peak picking
Grid assignment
Displacement fitting
Reconstruction of the pixel position
Saving into a detector definition file
Validation of the geometry with a 2D integration
Each module being made by lithographic processes, the error within a module will be assumed to be constant. We will use the name “displacement of the module” to describe the rigid movement of the module.
This tutorial uses data acquired by Marie Ruat from the ESRF detector group during the commissioning of the detector. The ID06-LVP is acknowledged for commissioning beam-time and fruitful discussion.
This detector contains 18 half-modules, each bound to a single CdTe monocrystal sensor and is designed for high energy X-ray radiation detection. Due to the construction procedure, these half-modules could show a misalignment within the detector plane. While the manufacturer (Dectris) guarantees a precision within a pixel (172µm), the misalignment of certain modules can be seen while calibrating Debye-Scherrer ring using reference sample. So the aim of this work is to provide a detector description with a better precision better than the original detector.
This work will be performed on the image of a grid available: http://www.silx.org/pub/pyFAI/detector_calibration
It is a good exercise to calibrate all rings of the later image using the pyFAI-calib2 tool. A calibration close to perfection is needed to visualize the module misalignment we aim at correcting.
%matplotlib inline
# %matplotlib nbagg
#many imports which will be used all along the notebook
import time
import pyFAI
import fabio
import numpy
from numpy.lib.stride_tricks import as_strided
from collections import namedtuple
from math import sin, cos, sqrt
import scipy
from scipy.ndimage import convolve, binary_dilation
from scipy.optimize import minimize
from matplotlib.pyplot import subplots
from pyFAI.ext.bilinear import Bilinear
from pyFAI.ext.watershed import InverseWatershed
from silx.resources import ExternalResources
from matplotlib import colors
if scipy.__version__ >= "0.18":
from scipy.spatial.distance import cdist as distance_matrix
else:
from scipy.spatial import distance_matrix
start_time = time.perf_counter()
print("Using pyFAI verison: ", pyFAI.version)
Using pyFAI verison: 2026.9.0-dev0
Triplet = namedtuple("Triplet", "left center right")
logcolor = colors.LogNorm(1e5, 3e5)
normcolor = colors.LogNorm(1, 2)
# Some compound types ...
dt = numpy.dtype([('y', numpy.float64),
('x', numpy.float64),
('i', numpy.int64)])
dl = numpy.dtype([('y', numpy.float64),
('x', numpy.float64),
('i', numpy.int64),
('Y', numpy.int64),
('X', numpy.int64)])
#Download all data:
#Nota: Configure here your proxy if you are behind a firewall
#os.environ["http_proxy"] = "http://proxy.comany.com:3128"
downloader = ExternalResources("detector_calibration", "http://www.silx.org/pub/pyFAI/detector_calibration/")
median21_left = downloader.getfile("Pilatus900kwID06_median21_left.npy")
median21_center = downloader.getfile("Pilatus900kwID06_median21_center.npy")
median21_right = downloader.getfile("Pilatus900kwID06_median21_right.npy")
mask_left = downloader.getfile("Pilatus900kwID06_mask_left.npy")
mask_center = downloader.getfile("Pilatus900kwID06_mask_center.npy")
mask_right = downloader.getfile("Pilatus900kwID06_mask_right.npy")
minimum = downloader.getfile("Pilatus900kwID06_minimum.npy")
img_left = fabio.open(median21_left).data
img_center = fabio.open(median21_center).data
img_right = fabio.open(median21_right).data
fig,ax = subplots(3, figsize=(20,6))
ax[0].set_title("Grid points acquired (after median filter)")
ax[0].imshow(img_left, interpolation="bilinear", norm=logcolor, cmap="inferno")
ax[1].imshow(img_center, interpolation="bilinear", norm=logcolor, cmap="inferno")
ax[2].imshow(img_right, interpolation="bilinear", norm=logcolor, cmap="inferno")
pass
def display(triplet, **kwargs):
_fig,ax = subplots(3, figsize=(20, 9))
ax[0].set_title("left")
ax[0].imshow(triplet.left, **kwargs)
ax[0].set_xlim(0, 1580)
ax[1].set_title("center")
ax[1].imshow(triplet.center, **kwargs)
ax[1].set_xlim(1400, 3010)
ax[2].set_title("right")
ax[2].imshow(triplet.right, **kwargs)
ax[2].set_xlim(2830, triplet.right.shape[-1]+5)
return ax
data = Triplet(img_left, img_center, img_right)
display(data, interpolation="bilinear", norm=logcolor, cmap="inferno");
Image processing#
There are 4 pre-processing steps which are needed.
Define for each module a unique identifier which will be used later on during the fitting procedure
Define the proper mask: each module is the assembly of 4x2 sub-modules and there are (3) interpolated pixels between each sub-module, such “unreliable pixels should be masked out as well
Correct the grid image by the smoothed image to have a constant background.
Convolve the raw image with a typical hole shape to allow a precise spotting of the hole center.
pilatus = pyFAI.detector_factory("Pilatus_900kw_CdTe")
print(pilatus)
print(pilatus.shape)
mask1 = pilatus.mask
module_size = pilatus.MODULE_SIZE
module_gap = pilatus.MODULE_GAP
submodule_size = (96,60)
Detector Pilatus CdTe 900kw PixelSize= 172µm, 172µm BottomRight (3)
(195, 4439)
#1 + 2 Calculation of the module_id and the interpolated-mask:
mid = numpy.zeros(pilatus.shape, dtype=int)
mask2 = numpy.zeros(pilatus.shape, dtype=int)
idx = 1
for i in range(1):
y_start = i*(module_gap[0] + module_size[0])
y_stop = y_start + module_size[0]
for j in range(9):
x_start = j*(module_gap[1] + module_size[1])
x_stop = x_start + module_size[1]
mid[y_start:y_stop,x_start: x_start+module_size[1]//2] = idx
idx+=1
mid[y_start:y_stop,x_start+module_size[1]//2: x_stop] = idx
idx+=1
mask2[y_start+submodule_size[0]-1:y_start+submodule_size[0]+2,
x_start:x_stop] = 1
for k in range(1,8):
mask2[y_start:y_stop,
x_start+k*(submodule_size[1]+1)-1:x_start+k*(submodule_size[1]+1)+2] = 1
fix, ax = subplots(2, figsize=(20,4))
ax[0].set_title("Module Id and inter-module mask")
ax[0].imshow(mid)
ax[1].imshow(mask2);
#Extra masking: bad pixels marked by the detector
mask0 = fabio.open(minimum).data<0
bad = numpy.where(mask0 | mask1 | mask2 | fabio.open(mask_left).data)
data.left[bad] = numpy.nan
bad = numpy.where(mask0 | mask1 | mask2 | fabio.open(mask_center).data)
data.center[bad] = numpy.nan
bad = numpy.where(mask0 | mask1 | mask2 | fabio.open(mask_right).data)
data.right[bad] = numpy.nan
display(data, interpolation="bilinear", norm=logcolor)
pass
# The Nan-masked image contains now only valid values (and Nan elsewhere). We will make a large median filter to
# build up a smooth image without gaps.
#
# This function is backported from future version of numpy ... it allows to expose a winbowed view
# to perform the nanmedian-filter
def sliding_window_view(x, shape, subok=False, readonly=True):
"""
Creates sliding window views of the N dimensional array with the given window
shape. Window slides across each dimension of `x` and extract subsets of `x`
at any window position.
Parameters
----------
x : array_like
Array to create sliding window views of.
shape : sequence of int
The shape of the window. Must have same length as the number of input array dimensions.
subok : bool, optional
If True, then sub-classes will be passed-through, otherwise the returned
array will be forced to be a base-class array (default).
readonly : bool, optional
If set to True, the returned array will always be readonly view.
Otherwise it will return writable copies(see Notes).
Returns
-------
view : ndarray
Sliding window views (or copies) of `x`. view.shape = x.shape - shape + 1
See also
--------
as_strided: Create a view into the array with the given shape and strides.
broadcast_to: broadcast an array to a given shape.
Notes
-----
``sliding_window_view`` create sliding window views of the N dimensions array
with the given window shape and its implementation based on ``as_strided``.
Please note that if readonly set to True, views are returned, not copies
of array. In this case, write operations could be unpredictable, so the returned
views are readonly. Bear in mind that returned copies (readonly=False) will
take more memory than the original array, due to overlapping windows.
For some cases there may be more efficient approaches to calculate transformations
across multi-dimensional arrays, for instance `scipy.signal.fftconvolve`, where combining
the iterating step with the calculation itself while storing partial results can result
in significant speedups.
Examples
--------
>>> i, j = np.ogrid[:3,:4]
>>> x = 10*i + j
>>> shape = (2,2)
>>> np.lib.stride_tricks.sliding_window_view(x, shape)
array([[[[ 0, 1],
[10, 11]],
[[ 1, 2],
[11, 12]],
[[ 2, 3],
[12, 13]]],
[[[10, 11],
[20, 21]],
[[11, 12],
[21, 22]],
[[12, 13],
[22, 23]]]])
"""
np = numpy
# first convert input to array, possibly keeping subclass
x = np.array(x, copy=False, subok=subok)
try:
shape = np.array(shape, np.int64)
except Exception:
raise TypeError('`shape` must be a sequence of integer')
else:
if shape.ndim > 1:
raise ValueError('`shape` must be one-dimensional sequence of integer')
if len(x.shape) != len(shape):
raise ValueError("`shape` length doesn't match with input array dimensions")
if np.any(shape <= 0):
raise ValueError('`shape` cannot contain non-positive value')
o = np.array(x.shape) - shape + 1 # output shape
if np.any(o <= 0):
raise ValueError('window shape cannot larger than input array shape')
if not isinstance(readonly, bool):
raise TypeError('readonly must be a boolean')
strides = x.strides
view_strides = strides
view_shape = np.concatenate((o, shape), axis=0)
view_strides = np.concatenate((view_strides, strides), axis=0)
view = as_strided(x, view_shape, view_strides, subok=subok, writeable=not readonly)
if not readonly:
return view.copy()
else:
return view
%%time
#Calculate a background image using a large median filter ... takes a while
shape = (13,13)
padded = Triplet(*(numpy.pad(i, tuple((i//2,) for i in shape), mode="edge") for i in data))
print(padded.left.shape)
(207, 4451)
CPU times: user 1.85 ms, sys: 0 ns, total: 1.85 ms
Wall time: 1.89 ms
%%time
background = Triplet(*[numpy.nanmedian(sliding_window_view(i, shape), axis = (-2,-1)) for i in padded])
print(background.left.shape)
<timed exec>:1: RuntimeWarning: All-NaN slice encountered
(195, 4439)
CPU times: user 11.4 s, sys: 1.12 s, total: 12.5 s
Wall time: 13.3 s
display(background, norm=logcolor, interpolation="bilinear");
normalized = Triplet(*(i/j for i,j in zip(data, background)))
display(normalized, interpolation="nearest", norm=normcolor);
fig,ax = subplots(1,3, figsize=(9,5))
ax[0].hist(normalized.left.ravel(), 100, range=(0,2))
ax[1].hist(normalized.center.ravel(), 100, range=(0,2))
ax[2].hist(normalized.right.ravel(), 100, range=(0,2));
For a precise measurement of the peak position, one trick is to convolve the image with a pattern which looks like a hole of the grid.
#Definition of the convolution kernel
ksize = 5
y,x = numpy.ogrid[-(ksize-1)//2:ksize//2+1,-(ksize-1)//2:ksize//2+1]
d = numpy.sqrt(y*y+x*x)
#Fade out curve definition
def fadeout(x):
return 1/(1+numpy.exp(3*(x-2.2)))
kernel = fadeout(d)
mini=kernel.sum()
print(mini)
fig,ax = subplots(1,3)
ax[0].imshow(d)
ax[0].set_title("Distance array")
ax[1].plot(numpy.linspace(0,5,100),fadeout(numpy.linspace(0,5,100)))
ax[1].set_title("fade-out curve")
ax[2].imshow(kernel)
ax[2].set_title("Convolution kernel");
15.439885086158014
my_smooth = Triplet(*(convolve(i, kernel, mode="constant", cval=0)/mini for i in normalized))
print(my_smooth.center.shape)
display(my_smooth);
(195, 4439)
all_masks = mask0 | mask1 | mask2
big_mask = binary_dilation(all_masks, iterations=ksize//2+1+1)
print(all_masks.sum(), big_mask.sum())
62453 208997
Peak picking#
We use the watershed module from pyFAI to retrieve all peak positions. Those regions are sieved out respectively for:
their size, it should be larger than the kernel itself
the peaks too close to masked regions are removed
the intensity of the peak
%%time
tmp = []
for i in my_smooth:
iw = InverseWatershed(i)
iw.init()
iw.merge_singleton()
all_regions = set(iw.regions.values())
regions = [i for i in all_regions if i.size>mini]
tmp.append(regions)
regions = Triplet(*tmp)
CPU times: user 2.16 s, sys: 387 ms, total: 2.55 s
Wall time: 2.78 s
#Remove peaks on masked region
sieved_region = Triplet(*([i for i in j if not big_mask[(i.index//pilatus.shape[-1], i.index%pilatus.shape[-1])]]
for j in regions))
print("Number of peaks not on masked areea : {} {} {}".format(len(sieved_region[0]),len(sieved_region[1]),len(sieved_region[2])))
Number of peaks not on masked areea : 4773 4772 4885
# Histogram of peak height:
s = Triplet(*(numpy.array([i.maxi for i in j]) for j in sieved_region))
fig, ax = subplots(3, figsize=(15,6))
[ax[i].hist(s[i], 100) for i in range(3)]
[ax[i].set_yscale("log") for i in range(3)];
#sieve-out for peak intensity
int_mini = 1.2
peaks = Triplet(*([(i.index//pilatus.shape[-1], i.index%pilatus.shape[-1]) for i in j if (i.maxi)>int_mini]
for j in sieved_region))
print("Number of remaining peaks with I>{}: {}".format(int_mini, [len(i) for i in peaks]))
peaks_raw = Triplet(*(numpy.array(i) for i in peaks))
Number of remaining peaks with I>1.2: [248, 242, 252]
# Finally the peak positions are interpolated using a second order taylor expansion
# in thevinicy of the maximum value of the signal:
#Create bilinear interpolator
bl = [Bilinear(i) for i in my_smooth]
#Overlay raw peak coordinate and refined peak positions
ref_peaks = [[b.local_maxi(p) for p in peaki] for b, peaki in zip(bl, peaks)]
ax = display(data)
peaks_ref = [numpy.array(i) for i in ref_peaks]
for i in range(3):
ax[i].plot(peaks_raw[i][:,1], peaks_raw[i][:, 0], ".r")
ax[i].plot(peaks_ref[i][:,1],peaks_ref[i][:, 0], ".b")
ax[0].set_title("Extracted peak position (red: raw, blue: refined)");
display(Triplet(mid, mid, mid));
At this stage we have about 3x250 peaks (with sub-pixel precision) which are visually distributed on all modules. Some modules have their peaks located along sub-module boundaries which are masked out, hence they have fewer control point for the calculation. Let’s assign each peak to a module identifier. This allows to print out the number of peaks per module:
yxi = Triplet(*[numpy.array([i+(mid[round(i[0]),round(i[1])],)
for i in j], dtype=dt)
for j in ref_peaks])
print("Number of keypoint per module:")
cp = Triplet([numpy.nan], [numpy.nan], [numpy.nan])
for i in range(1,mid.max()+1):
cp.left.append((yxi.left[:]["i"] == i).sum())
cp.center.append((yxi.center[:]["i"] == i).sum())
cp.right.append((yxi.right[:]["i"] == i).sum())
print("Module id:",i,
"left cp:", (yxi.left[:]["i"] == i).sum(),
"center cp:", (yxi.center[:]["i"] == i).sum(),
"right cp:", (yxi.right[:]["i"] == i).sum())
fig, ax = subplots(figsize=(8,6))
ax.plot(cp.left, "-or", label="left")
ax.plot(cp.center,"-og", label="center")
ax.plot(cp.right,"-ob", label="right")
ax.set_ylabel("Number of control point")
ax.set_xlabel("Module id")
ax.set_xticks(numpy.arange(1, 19))
ax.legend()
ax.set_title("Overlapping of the 3 grid positions");
Number of keypoint per module:
Module id: 1 left cp: 33 center cp: 0 right cp: 0
Module id: 2 left cp: 48 center cp: 0 right cp: 0
Module id: 3 left cp: 29 center cp: 0 right cp: 0
Module id: 4 left cp: 42 center cp: 0 right cp: 0
Module id: 5 left cp: 42 center cp: 0 right cp: 0
Module id: 6 left cp: 36 center cp: 12 right cp: 0
Module id: 7 left cp: 18 center cp: 27 right cp: 0
Module id: 8 left cp: 0 center cp: 48 right cp: 0
Module id: 9 left cp: 0 center cp: 36 right cp: 0
Module id: 10 left cp: 0 center cp: 41 right cp: 0
Module id: 11 left cp: 0 center cp: 48 right cp: 0
Module id: 12 left cp: 0 center cp: 24 right cp: 24
Module id: 13 left cp: 0 center cp: 6 right cp: 30
Module id: 14 left cp: 0 center cp: 0 right cp: 48
Module id: 15 left cp: 0 center cp: 0 right cp: 42
Module id: 16 left cp: 0 center cp: 0 right cp: 30
Module id: 17 left cp: 0 center cp: 0 right cp: 48
Module id: 18 left cp: 0 center cp: 0 right cp: 30
Grid assignment#
The calibration is performed using a regular grid, the idea is to assign to each peak of coordinates (x,y) the integer value (X, Y) which correspond to the grid coordinate system.
The first step is to measure the grid pitch which correspond to the distance (in pixels) from one peak to the next. This is easily obtained from a pair-wise distribution function.
# pairwise distance calculation using scipy.spatial.distance_matrix
dist = [distance_matrix(i, i) for i in peaks_ref]
fig, ax = subplots(3,figsize=(15,6))
for i in range(3):
ax[i].hist(dist[i].ravel(), 250, range=(0,100))
ax[0].set_title("Pair-wise distribution function")
ax[2].set_xlabel("distance in pixel");
The histogram of the pair-distribution function has a first peak at 0 and the second peak between 29 and 30. Let’s start the fit with this value
Two other parameters correspond to the offset, in pixel for the grid index (X,Y) = (0,0). The easiest is to measure the smallest x and y for the first module.
#from pair-wise distribution histogram
step = 29.3
def index_module(module, position, step):
"Return the peak position for the given module at the grid position. The guess parameter is also provided"
indexed_module = yxi[position][yxi[position][:]["i"] == module]
y_min = indexed_module["y"].min()
x_min = indexed_module["x"].min()
print("offset for the first peak: ", x_min, y_min)
indexed = numpy.zeros(indexed_module.size, dtype=dl)
delta_max = 0
for i,v in enumerate(indexed_module):
Y = round((v["y"] - y_min)/step)
X = round((v["x"] - x_min)/step)
indexed[i]["y"] = v["y"]
indexed[i]["x"] = v["x"]
indexed[i]["i"] = v["i"]
indexed[i]["Y"] = Y
indexed[i]["X"] = X
delta_max = max(delta_max, sqrt((v["y"]-Y*step-y_min)**2 + (v["x"]-X*step-x_min)**2)/step)
print(f"peak id: {i!s:>2} {v!s:>35} Y:{Y:d} (Δ={(v['y']-Y*step-y_min)/step:.3f}) X:{X} (Δ={(v['x']-X*step-x_min)/step:.3f})")
if delta_max>0.1:
print("Assignment is error prone with delta_max (in steps) =", delta_max)
guess = [step, y_min, x_min, 0]
return indexed, guess
#work with the first module and fit the peak positions
indexed1, guess1 = index_module(1, 0, step)
guess1
offset for the first peak: 2.4549330472946167 6.0
peak id: 0 (123.26432752609253, 2.4915760159492493, 1) Y:4 (Δ=0.002) X:0 (Δ=0.001)
peak id: 1 (123.30457380414009, 31.675586879253387, 1) Y:4 (Δ=0.004) X:1 (Δ=-0.003)
peak id: 2 (123.38710975646973, 90.3087826371193, 1) Y:4 (Δ=0.006) X:3 (Δ=-0.002)
peak id: 3 (123.45950040221214, 148.81972688436508, 1) Y:4 (Δ=0.009) X:5 (Δ=-0.005)
peak id: 4 (64.64053726196289, 2.4587920904159546, 1) Y:2 (Δ=0.001) X:0 (Δ=0.000)
peak id: 5 (64.70183858275414, 31.67507576942444, 1) Y:2 (Δ=0.003) X:1 (Δ=-0.003)
peak id: 6 (64.7810900658369, 90.26187604665756, 1) Y:2 (Δ=0.006) X:3 (Δ=-0.003)
peak id: 7 (64.8662526011467, 148.85133849084377, 1) Y:2 (Δ=0.009) X:5 (Δ=-0.004)
peak id: 8 (64.93572010844946, 207.4145676791668, 1) Y:2 (Δ=0.011) X:7 (Δ=-0.005)
peak id: 9 (64.98832104168832, 236.73783913254738, 1) Y:2 (Δ=0.013) X:8 (Δ=-0.004)
peak id: 10 (35.373406022787094, 2.4549330472946167, 1) Y:1 (Δ=0.003) X:0 (Δ=0.000)
peak id: 11 (35.44373828172684, 31.63500201702118, 1) Y:1 (Δ=0.005) X:1 (Δ=-0.004)
peak id: 12 (152.50069576501846, 2.5122069716453552, 1) Y:5 (Δ=0.000) X:0 (Δ=0.002)
peak id: 13 (152.5614824295044, 31.659371316432953, 1) Y:5 (Δ=0.002) X:1 (Δ=-0.003)
peak id: 14 (152.65835246443748, 90.2640967965126, 1) Y:5 (Δ=0.005) X:3 (Δ=-0.003)
peak id: 15 (152.81499536335468, 148.83686193823814, 1) Y:5 (Δ=0.011) X:5 (Δ=-0.004)
peak id: 16 (152.9734156038612, 236.64661991596222, 1) Y:5 (Δ=0.016) X:8 (Δ=-0.007)
peak id: 17 (181.78467336297035, 2.511962652206421, 1) Y:6 (Δ=-0.001) X:0 (Δ=0.002)
peak id: 18 (181.8645792156458, 31.708801716566086, 1) Y:6 (Δ=0.002) X:1 (Δ=-0.002)
peak id: 19 (181.97041345015168, 90.25578546524048, 1) Y:6 (Δ=0.006) X:3 (Δ=-0.003)
peak id: 20 (182.12500229477882, 148.84423378109932, 1) Y:6 (Δ=0.011) X:5 (Δ=-0.004)
peak id: 21 (182.2299488633871, 207.375379383564, 1) Y:6 (Δ=0.015) X:7 (Δ=-0.006)
peak id: 22 (182.2969901561737, 236.66873440146446, 1) Y:6 (Δ=0.017) X:8 (Δ=-0.006)
peak id: 23 (6.161871328949928, 31.61624926328659, 1) Y:0 (Δ=0.006) X:1 (Δ=-0.005)
peak id: 24 (6.0, 90.0, 1) Y:0 (Δ=0.000) X:3 (Δ=-0.012)
peak id: 25 (6.265571027994156, 148.82681784033775, 1) Y:0 (Δ=0.009) X:5 (Δ=-0.004)
peak id: 26 (6.375751584768295, 207.4099151790142, 1) Y:0 (Δ=0.013) X:7 (Δ=-0.005)
peak id: 27 (6.383204132318497, 236.71087712049484, 1) Y:0 (Δ=0.013) X:8 (Δ=-0.005)
peak id: 28 (123.55799156427383, 207.39807868003845, 1) Y:4 (Δ=0.012) X:7 (Δ=-0.005)
peak id: 29 (123.63462010025978, 236.7026522755623, 1) Y:4 (Δ=0.015) X:8 (Δ=-0.005)
peak id: 30 (35.5145745575428, 90.29551619291306, 1) Y:1 (Δ=0.007) X:3 (Δ=-0.002)
peak id: 31 (35.616493225097656, 207.4486949145794, 1) Y:1 (Δ=0.011) X:7 (Δ=-0.004)
peak id: 32 (35.670637756586075, 236.7270960509777, 1) Y:1 (Δ=0.013) X:8 (Δ=-0.004)
[29.3, np.float64(6.0), np.float64(2.4549330472946167), 0]
The grid looks very well aligned with the axes which makes this step easier but nothing guarantees it is perfect, so the rotation of the grid has to be measured as well.
The default rotation will be zero and will be fitted later on.
Once the indexes X,Y determined for each peak, one can fit the parameter to properly align the grid with the first module. Those 4 parameters are step-size, x_min, y_min and angle
# Align center grid on module #9
indexed9, guess9 = index_module(9, 1, step)
guess9
offset for the first peak: 1983.7893059700727 10.063118577003479
peak id: 0 (68.50939068198204, 1983.955585412681, 9) Y:2 (Δ=-0.005) X:0 (Δ=0.006)
peak id: 1 (68.56470796465874, 2013.342754483223, 9) Y:2 (Δ=-0.003) X:1 (Δ=0.009)
peak id: 2 (68.60563236474991, 2042.6800186634064, 9) Y:2 (Δ=-0.002) X:2 (Δ=0.010)
peak id: 3 (68.62543919682503, 2072.044121902436, 9) Y:2 (Δ=-0.001) X:3 (Δ=0.012)
peak id: 4 (68.69580888748169, 2130.7961300760508, 9) Y:2 (Δ=0.001) X:5 (Δ=0.017)
peak id: 5 (68.71686375141144, 2189.504914909601, 9) Y:2 (Δ=0.002) X:7 (Δ=0.021)
peak id: 6 (10.063118577003479, 1983.7893059700727, 9) Y:0 (Δ=0.000) X:0 (Δ=0.000)
peak id: 7 (10.103013597428799, 2013.1693960279226, 9) Y:0 (Δ=0.001) X:1 (Δ=0.003)
peak id: 8 (10.117956094443798, 2042.5244252085686, 9) Y:0 (Δ=0.002) X:2 (Δ=0.005)
peak id: 9 (10.157146647572517, 2071.906115964055, 9) Y:0 (Δ=0.003) X:3 (Δ=0.007)
peak id: 10 (10.236845061182976, 2130.6630921661854, 9) Y:0 (Δ=0.006) X:5 (Δ=0.013)
peak id: 11 (10.253325790166855, 2189.4553923606873, 9) Y:0 (Δ=0.006) X:7 (Δ=0.019)
peak id: 12 (126.95256949961185, 1984.1226668655872, 9) Y:4 (Δ=-0.011) X:0 (Δ=0.011)
peak id: 13 (126.95932010188699, 2013.439321309328, 9) Y:4 (Δ=-0.010) X:1 (Δ=0.012)
peak id: 14 (126.97604274377227, 2042.7918131649494, 9) Y:4 (Δ=-0.010) X:2 (Δ=0.014)
peak id: 15 (127.088155426085, 2072.165059968829, 9) Y:4 (Δ=-0.006) X:3 (Δ=0.016)
peak id: 16 (127.16240228712559, 2130.873685270548, 9) Y:4 (Δ=-0.003) X:5 (Δ=0.020)
peak id: 17 (127.21290422976017, 2189.6241732537746, 9) Y:4 (Δ=-0.002) X:7 (Δ=0.025)
peak id: 18 (156.1566076427698, 1984.2077122628689, 9) Y:5 (Δ=-0.014) X:0 (Δ=0.014)
peak id: 19 (156.1969578564167, 2013.5341706573963, 9) Y:5 (Δ=-0.012) X:1 (Δ=0.015)
peak id: 20 (156.25369757413864, 2042.8916319608688, 9) Y:5 (Δ=-0.011) X:2 (Δ=0.017)
peak id: 21 (156.28958761692047, 2072.229489967227, 9) Y:5 (Δ=-0.009) X:3 (Δ=0.018)
peak id: 22 (156.35255643725395, 2130.940498199314, 9) Y:5 (Δ=-0.007) X:5 (Δ=0.022)
peak id: 23 (156.48744174838066, 2189.6202401816845, 9) Y:5 (Δ=-0.003) X:7 (Δ=0.025)
peak id: 24 (39.492495000362396, 2189.4764204621315, 9) Y:1 (Δ=0.004) X:7 (Δ=0.020)
peak id: 25 (185.56176635622978, 2131.0037491295952, 9) Y:6 (Δ=-0.010) X:5 (Δ=0.024)
peak id: 26 (185.66520437598228, 2189.68753400445, 9) Y:6 (Δ=-0.007) X:7 (Δ=0.027)
peak id: 27 (185.36895179748535, 1984.2666456401348, 9) Y:6 (Δ=-0.017) X:0 (Δ=0.016)
peak id: 28 (185.3837212920189, 2013.5953775644302, 9) Y:6 (Δ=-0.016) X:1 (Δ=0.017)
peak id: 29 (185.42883336544037, 2042.9286718666553, 9) Y:6 (Δ=-0.015) X:2 (Δ=0.018)
peak id: 30 (185.49475926160812, 2072.294913351536, 9) Y:6 (Δ=-0.013) X:3 (Δ=0.021)
peak id: 31 (39.24570585787296, 1983.900622650981, 9) Y:1 (Δ=-0.004) X:0 (Δ=0.004)
peak id: 32 (39.372827231884, 2013.2331027388573, 9) Y:1 (Δ=0.000) X:1 (Δ=0.005)
peak id: 33 (39.37062767148018, 2042.551634311676, 9) Y:1 (Δ=0.000) X:2 (Δ=0.006)
peak id: 34 (39.30705660581589, 2071.980400722474, 9) Y:1 (Δ=-0.002) X:3 (Δ=0.010)
peak id: 35 (39.45478484034538, 2130.6488406956196, 9) Y:1 (Δ=0.003) X:5 (Δ=0.012)
[29.3, np.float64(10.063118577003479), np.float64(1983.7893059700727), 0]
The error in positioning each of the pixel is less than 0.1 pixel which is already excellent and will allow a straight forward fit.
Let’s do the same for the left and right grid positions:
# Align left grid on module #6
indexed6, guess6 = index_module(6, 0, step)
guess6
offset for the first peak: 1262.7836351692677 6.587490230798721
peak id: 0 (65.24553410708904, 1262.8104900866747, 6) Y:2 (Δ=0.002) X:0 (Δ=0.001)
peak id: 1 (65.20108564198017, 1321.4461102187634, 6) Y:2 (Δ=0.000) X:2 (Δ=0.002)
peak id: 2 (65.147307574749, 1380.080051727593, 6) Y:2 (Δ=-0.001) X:4 (Δ=0.003)
peak id: 3 (65.14464166760445, 1409.4337121546268, 6) Y:2 (Δ=-0.001) X:5 (Δ=0.005)
peak id: 4 (65.09326767921448, 1438.7336745858192, 6) Y:2 (Δ=-0.003) X:6 (Δ=0.005)
peak id: 5 (65.13473019003868, 1468.098834067583, 6) Y:2 (Δ=-0.002) X:7 (Δ=0.007)
peak id: 6 (153.14817793667316, 1262.7902310788631, 6) Y:5 (Δ=0.002) X:0 (Δ=0.000)
peak id: 7 (153.0979304537177, 1321.4452154040337, 6) Y:5 (Δ=0.000) X:2 (Δ=0.002)
peak id: 8 (153.00942741893232, 1380.082072339952, 6) Y:5 (Δ=-0.003) X:4 (Δ=0.003)
peak id: 9 (152.98925394751132, 1409.3811523616314, 6) Y:5 (Δ=-0.003) X:5 (Δ=0.003)
peak id: 10 (152.95016007497907, 1438.7361478805542, 6) Y:5 (Δ=-0.005) X:6 (Δ=0.005)
peak id: 11 (152.92198193073273, 1468.029538910836, 6) Y:5 (Δ=-0.006) X:7 (Δ=0.005)
peak id: 12 (182.46851214766502, 1262.8084784001112, 6) Y:6 (Δ=0.003) X:0 (Δ=0.001)
peak id: 13 (182.36686637997627, 1321.4549279510975, 6) Y:6 (Δ=-0.001) X:2 (Δ=0.002)
peak id: 14 (182.29776313900948, 1380.0563338547945, 6) Y:6 (Δ=-0.003) X:4 (Δ=0.002)
peak id: 15 (182.25438725948334, 1409.3934617042542, 6) Y:6 (Δ=-0.005) X:5 (Δ=0.004)
peak id: 16 (182.2208000421524, 1438.6919509470463, 6) Y:6 (Δ=-0.006) X:6 (Δ=0.004)
peak id: 17 (182.16069494187832, 1467.998027174035, 6) Y:6 (Δ=-0.008) X:7 (Δ=0.004)
peak id: 18 (6.6851504147052765, 1262.7944948524237, 6) Y:0 (Δ=0.003) X:0 (Δ=0.000)
peak id: 19 (6.640458732843399, 1321.4577689766884, 6) Y:0 (Δ=0.002) X:2 (Δ=0.003)
peak id: 20 (6.619248270988464, 1380.1239314079285, 6) Y:0 (Δ=0.001) X:4 (Δ=0.005)
peak id: 21 (6.587490230798721, 1409.4567930102348, 6) Y:0 (Δ=0.000) X:5 (Δ=0.006)
peak id: 22 (6.625625342130661, 1438.7712363898754, 6) Y:0 (Δ=0.001) X:6 (Δ=0.006)
peak id: 23 (6.620771706104279, 1468.0859036073089, 6) Y:0 (Δ=0.001) X:7 (Δ=0.007)
peak id: 24 (123.88934447616339, 1262.7836351692677, 6) Y:4 (Δ=0.003) X:0 (Δ=0.000)
peak id: 25 (123.81679327785969, 1321.431401103735, 6) Y:4 (Δ=0.001) X:2 (Δ=0.002)
peak id: 26 (123.73721066117287, 1380.0792014226317, 6) Y:4 (Δ=-0.002) X:4 (Δ=0.003)
peak id: 27 (123.70474091172218, 1409.4381517469883, 6) Y:4 (Δ=-0.003) X:5 (Δ=0.005)
peak id: 28 (123.67492124438286, 1438.714588880539, 6) Y:4 (Δ=-0.004) X:6 (Δ=0.004)
peak id: 29 (123.69108924269676, 1468.0051446286961, 6) Y:4 (Δ=-0.003) X:7 (Δ=0.004)
peak id: 30 (35.96894356608391, 1262.8037863075733, 6) Y:1 (Δ=0.003) X:0 (Δ=0.001)
peak id: 31 (35.91596709191799, 1321.4524047076702, 6) Y:1 (Δ=0.001) X:2 (Δ=0.002)
peak id: 32 (35.901947505772114, 1380.1000199243426, 6) Y:1 (Δ=0.000) X:4 (Δ=0.004)
peak id: 33 (35.88425403833389, 1409.4048537909985, 6) Y:1 (Δ=-0.000) X:5 (Δ=0.004)
peak id: 34 (35.86844399571419, 1438.7647203952074, 6) Y:1 (Δ=-0.001) X:6 (Δ=0.006)
peak id: 35 (35.83115614950657, 1468.1130942106247, 6) Y:1 (Δ=-0.002) X:7 (Δ=0.008)
[29.3, np.float64(6.587490230798721), np.float64(1262.7836351692677), 0]
# Align right grid on module #12
indexed12, guess12 = index_module(12, 2, step)
guess12
offset for the first peak: 2852.760150760412 8.480030238628387
peak id: 0 (66.95120572298765, 2852.876041546464, 12) Y:2 (Δ=-0.004) X:0 (Δ=0.004)
peak id: 1 (66.99095541704446, 2882.1832368671894, 12) Y:2 (Δ=-0.003) X:1 (Δ=0.004)
peak id: 2 (67.06964544206858, 2911.478492349386, 12) Y:2 (Δ=-0.000) X:2 (Δ=0.004)
peak id: 3 (67.11765176802874, 2940.8254491239786, 12) Y:2 (Δ=0.001) X:3 (Δ=0.006)
peak id: 4 (154.6803744137287, 2853.030448032543, 12) Y:5 (Δ=-0.010) X:0 (Δ=0.009)
peak id: 5 (154.74708157777786, 2882.2763251662254, 12) Y:5 (Δ=-0.008) X:1 (Δ=0.007)
peak id: 6 (154.80892977118492, 2911.6114336252213, 12) Y:5 (Δ=-0.006) X:2 (Δ=0.009)
peak id: 7 (154.89501137286425, 2940.91354906559, 12) Y:5 (Δ=-0.003) X:3 (Δ=0.009)
peak id: 8 (184.08287701755762, 2940.9527590870857, 12) Y:6 (Δ=-0.007) X:3 (Δ=0.010)
peak id: 9 (184.04450400918722, 2911.6435011923313, 12) Y:6 (Δ=-0.008) X:2 (Δ=0.010)
peak id: 10 (183.96863915026188, 2882.337095975876, 12) Y:6 (Δ=-0.011) X:1 (Δ=0.009)
peak id: 11 (183.8828891068697, 2853.0624732486904, 12) Y:6 (Δ=-0.014) X:0 (Δ=0.010)
peak id: 12 (8.664518505334854, 2940.7563968598843, 12) Y:0 (Δ=0.006) X:3 (Δ=0.003)
peak id: 13 (8.597591191530228, 2911.4043534100056, 12) Y:0 (Δ=0.004) X:2 (Δ=0.002)
peak id: 14 (8.558971166610718, 2882.099878259003, 12) Y:0 (Δ=0.003) X:1 (Δ=0.001)
peak id: 15 (37.70624363422394, 2852.841973081231, 12) Y:1 (Δ=-0.003) X:0 (Δ=0.003)
peak id: 16 (37.77454175055027, 2882.1468552798033, 12) Y:1 (Δ=-0.000) X:1 (Δ=0.003)
peak id: 17 (37.81811560690403, 2911.4415976405144, 12) Y:1 (Δ=0.001) X:2 (Δ=0.003)
peak id: 18 (37.87953828275204, 2940.7823995798826, 12) Y:1 (Δ=0.003) X:3 (Δ=0.004)
peak id: 19 (8.480030238628387, 2852.760150760412, 12) Y:0 (Δ=0.000) X:0 (Δ=0.000)
peak id: 20 (125.42988848686218, 2852.9501489438117, 12) Y:4 (Δ=-0.009) X:0 (Δ=0.006)
peak id: 21 (125.4991235435009, 2882.278958082199, 12) Y:4 (Δ=-0.006) X:1 (Δ=0.007)
peak id: 22 (125.53714892268181, 2911.5333232581615, 12) Y:4 (Δ=-0.005) X:2 (Δ=0.006)
peak id: 23 (125.6428810954094, 2940.864393338561, 12) Y:4 (Δ=-0.001) X:3 (Δ=0.007)
[29.3, np.float64(8.480030238628387), np.float64(2852.760150760412), 0]
The error in positioning each of the pixel is less than 0.1 pixel which is already excellent and will allow a straight forward fit.
All rotations will be performed around the center of each module:
#Calculate the center of every single module for rotation around this center.
centers = {i: numpy.array([[numpy.where(mid == i)[1].mean()], [numpy.where(mid == i)[0].mean()]]) for i in range(1, 19)}
for k,v in centers.items():
print(k,v.ravel())
1 [121. 97.]
2 [364.5 97. ]
3 [615. 97.]
4 [858.5 97. ]
5 [1109. 97.]
6 [1352.5 97. ]
7 [1603. 97.]
8 [1846.5 97. ]
9 [2097. 97.]
10 [2340.5 97. ]
11 [2591. 97.]
12 [2834.5 97. ]
13 [3085. 97.]
14 [3328.5 97. ]
15 [3579. 97.]
16 [3822.5 97. ]
17 [4073. 97.]
18 [4316.5 97. ]
# Define a rotation of a module around the center of the module ...
def rotate(angle, xy, module):
"Perform the rotation of the xy points around the center of the given module"
rot = [[cos(angle),-sin(angle)],
[sin(angle), cos(angle)]]
center = centers[module]
return numpy.dot(rot, xy - center) + center
Fit the grid on a reference module for each position#
The cost function for the reference module for each grid position is calculated as the sum of distances squared in pixel space. It uses 4 parameters which are step-size, y_min, x_min, and angle
def cost_grid(param, module, indexed):
"""Cost function for moving the grid on a reference module
contains: step, y_min, x_min, angle
returns the sum of distance squared in pixel space
"""
step = param[0]
y_min = param[1]
x_min = param[2]
angle = param[3]
XY = numpy.vstack((indexed["X"], indexed["Y"]))
xy_min = [[x_min], [y_min]]
xy_guess = rotate(angle, step * XY + xy_min, module)
delta = xy_guess - numpy.vstack((indexed["x"], indexed["y"]))
return (delta*delta).sum()
def fit_grid(module, position, guess, indexed):
"""
:param module:
:return: fit result of the grid aligned on the module
"""
where = "left center right".split()[position]
print(f"Align {where} position on module #{module}")
print(f"Before optimization {guess} cost=", cost_grid(guess, module, indexed))
res = minimize(cost_grid, guess, (module, indexed), method = "slsqp")
print(res)
print("Average displacement (pixels): ",sqrt(res.fun/len(indexed)))
return res
# Alignment on the left side
res6 = fit_grid(6, 0, guess6, indexed6)
print("#"*50)
# Alignment on the center
res9 = fit_grid(9, 1, guess9, indexed9)
print("#"*50)
# Alignment on the right side
res12 = fit_grid(12, 2, guess12, indexed12)
Align left position on module #6
Before optimization [29.3, np.float64(6.587490230798721), np.float64(1262.7836351692677), 0] cost= 0.8042444009621303
message: Optimization terminated successfully
success: True
status: 0
fun: 0.3216010313254031
x: [ 2.930e+01 6.559e+00 1.263e+03 -3.512e-04]
nit: 5
jac: [-8.322e-04 -9.259e-05 -1.180e-04 -3.862e-02]
nfev: 39
njev: 5
multipliers: []
Average displacement (pixels): 0.09451646407510808
##################################################
Align center position on module #9
Before optimization [29.3, np.float64(10.063118577003479), np.float64(1983.7893059700727), 0] cost= 9.682972435951925
message: Optimization terminated successfully
success: True
status: 0
fun: 2.745286093010121
x: [ 2.930e+01 9.926e+00 1.984e+03 -3.053e-04]
nit: 5
jac: [-2.084e-03 -5.658e-03 5.799e-03 3.719e-01]
nfev: 40
njev: 5
multipliers: []
Average displacement (pixels): 0.27614841485704555
##################################################
Align right position on module #12
Before optimization [29.3, np.float64(8.480030238628387), np.float64(2852.760150760412), 0] cost= 1.598977061764155
message: Optimization terminated successfully
success: True
status: 0
fun: 0.37497580663587093
x: [ 2.925e+01 8.558e+00 2.853e+03 -6.091e-04]
nit: 7
jac: [ 9.835e-05 -1.200e-04 4.818e-05 -1.089e-03]
nfev: 47
njev: 7
multipliers: []
Average displacement (pixels): 0.12499596770760762
At this step, the grid is perfectly aligned with the first module. This module is used as the reference one and all other are aligned along it, using this first fit:
def index_positon(ref_module, position, guess, thres=1):
"""Return the indexed peak position for all the module for the grid position
"""
where = "left center right".split()[position]
print(f"Aligning all peak positions of {where} position on module #{ref_module}")
step, y_min, x_min, angle = guess
xy = numpy.vstack((yxi[position]["x"], yxi[position]["y"]))
indexed = numpy.zeros(xy.shape[1], dtype=dl)
indexed["y"] = yxi[position]["y"]
indexed["x"] = yxi[position]["x"]
indexed["i"] = yxi[position]["i"]
xy_min = [[x_min], [y_min]]
XY_app = (rotate(-angle, xy, module=ref_module) - xy_min) / step
XY_int = numpy.round((XY_app)).astype("int")
indexed["X"] = XY_int[0]
indexed["Y"] = XY_int[1]
rotate(angle, step * XY_int + xy_min, module=ref_module)
err = (XY_app - XY_int)*step
delta = numpy.sqrt((err**2).sum(axis=0))
print(f"suspicious: {(delta>thres).sum()} / {delta.size} with threshold {thres}:")
suspicious = indexed[numpy.where(abs(delta>thres))]
print(suspicious)
return indexed
print(len(index_positon(9, 1, guess=res9.x, thres=3)))
print(len(index_positon(6, 0, guess=res6.x, thres=1)))
print(len(index_positon(12, 2, guess=res12.x, thres=6)))
Aligning all peak positions of center position on module #9
suspicious: 8 / 242 with threshold 3:
[(69.70373777, 2983.28384909, 13, 2, 34)
(69.43861058, 2924.7600067 , 12, 2, 32)
(11.03073646, 2866.03510865, 12, 0, 30)
(11.06763498, 2924.90741896, 12, 0, 32)
(11.38562825, 2983.55737239, 13, 0, 34)
(40.50820342, 2983.44942433, 13, 1, 34)
(40.21956874, 2865.98224925, 12, 1, 30)
(40.25905779, 2924.8338756 , 12, 1, 32)]
242
Aligning all peak positions of left position on module #6
suspicious: 12 / 248 with threshold 1:
[(123.26432753, 2.49157602, 1, 4, -43)
(123.3045738 , 31.67558688, 1, 4, -42)
( 64.64053726, 2.45879209, 1, 2, -43)
( 64.70183858, 31.67507577, 1, 2, -42)
( 35.37340602, 2.45493305, 1, 1, -43)
( 35.44373828, 31.63500202, 1, 1, -42)
(152.50069577, 2.51220697, 1, 5, -43)
(152.56148243, 31.65937132, 1, 5, -42)
(181.78467336, 2.51196265, 1, 6, -43)
(181.86457922, 31.70880172, 1, 6, -42)
( 7. , 412. , 2, 0, -29)
( 6. , 90. , 1, 0, -40)]
248
Aligning all peak positions of right position on module #12
suspicious: 8 / 252 with threshold 6:
[( 70.40255883, 4408.11720203, 18, 2, 53)
( 70.33312753, 4349.40424293, 18, 2, 51)
( 11.99924202, 4408.40927956, 18, 0, 53)
( 11.86846654, 4349.57377118, 18, 0, 51)
( 11.78572394, 4290.85715419, 18, 0, 49)
( 41.09010582, 4349.50418448, 18, 1, 51)
( 41.16446756, 4408.23226897, 18, 1, 53)
(128.84898977, 4407.8465144 , 18, 4, 53)]
252
Only 7 peaks have an initial displacement of more than 6 pixels, all located in module 18, which is the furthest away from module 12. All other assignment are in 3 pixel on the left side and 1 pixel in the central position. The visual inspection confirms all localizations are valid.
There are 18 (half-)modules which have each of them 2 translations and one rotation. Only 7 of them are fitted in the first step. In addition to the step size, this represents 22 degrees of freedom for the fit. The first module is used to align the grid, all other modules are then aligned along this grid.
#his contains all peaks with their index
indexed = Triplet(index_positon(6, 0, guess=res6.x, thres=7),
index_positon(9, 1, guess=res9.x, thres=7),
index_positon(12, 2, guess=res12.x, thres=7))
Aligning all peak positions of left position on module #6
suspicious: 0 / 248 with threshold 7:
[]
Aligning all peak positions of center position on module #9
suspicious: 0 / 242 with threshold 7:
[]
Aligning all peak positions of right position on module #12
suspicious: 0 / 252 with threshold 7:
[]
The submodule cost function is the sum of the squares of the difference in the pixel space:
#here are defined the piot point for switching reference:
pivots = [6, 9, 12]
def submodule_cost(param, module, position):
"""contains: step,
y_min_6L, x_min_6L, angle_6L,
y_min_9C, x_min_9C, angle_9C,
y_min_12R, x_min_12R, angle_12R,
y_min_1, x_min_1, angle_1,
y_min_2, x_min_2, angle_2,
y_min_3, x_min_3, angle_3, ...
:param: array with 64 parameters
:param module: module number from 1 to 18
:param position: 0, 1 or 2
:returns: the sum of distance squared in pixel space for the given module with the given grid.
"""
step = param[0]
mask = indexed[position]["i"] == module
if mask.sum() == 0 :
return 0
substack = indexed[position][mask]
pivot = pivots[position]
y_min_grid, x_min_grid, angle_grid = param[1+3*(position): 4+3*(position)]
XY = numpy.vstack((substack["X"], substack["Y"]))
xy_min_grid = numpy.array([[x_min_grid], [y_min_grid]])
xy_guess1 = rotate(angle_grid, step * XY + xy_min_grid, pivot)
#print(y_min_grid, x_min_grid, angle_grid)
if module == pivot:
#print("Not much to do for reference module as it is naturally alligned")
delta = xy_guess1 - numpy.vstack((substack["x"], substack["y"]))
else:
"perform the correction for given module"
y_min, x_min, angle = param[7+3*module: 10+3*module]
xy_min = numpy.array([[x_min], [y_min]])
xy_guess = rotate(angle, xy_guess1 + xy_min, module)
delta = xy_guess - numpy.vstack((substack["x"], substack["y"]))
#print(y_min, x_min, angle)
return (delta*delta).sum()
def print_res64(param):
res = ["step: {:.3f}".format(param[0])]
def f(p):
return "Δx: {:8.3f}, Δy: {:6.3f} rot: {:6.3f}°".format(p[1], p[0], numpy.rad2deg(p[2]))
res.append(f" {pivots[0]}L: {f(param[1:4])}")
res.append(f" {pivots[1]}C: {f(param[4:7])}")
res.append(f"{pivots[2]}R: {f(param[7:10])}")
for i in range(1,19):
w = "L" if i<min(pivots) else "R" if i>max(pivots) else "C"
res.append(f"{i:2d}{w}: " + f(param[7+3*i:10+3*i]))
print("\n".join(res))
submodule_cost(numpy.zeros(64), 1, 0)
np.float64(1155470.2965653066)
# Evaluated the guess and print the cost:
guess64 = numpy.zeros(64)
guess64[:4] = res6.x
guess64[4:7] = res9.x[1:]
guess64[7:10] = res12.x[1:]
print_res64(guess64)
step: 29.303
6L: Δx: 1262.878, Δy: 6.559 rot: -0.020°
9C: Δx: 1984.210, Δy: 9.926 rot: -0.017°
12R: Δx: 2852.995, Δy: 8.558 rot: -0.035°
1L: Δx: 0.000, Δy: 0.000 rot: 0.000°
2L: Δx: 0.000, Δy: 0.000 rot: 0.000°
3L: Δx: 0.000, Δy: 0.000 rot: 0.000°
4L: Δx: 0.000, Δy: 0.000 rot: 0.000°
5L: Δx: 0.000, Δy: 0.000 rot: 0.000°
6C: Δx: 0.000, Δy: 0.000 rot: 0.000°
7C: Δx: 0.000, Δy: 0.000 rot: 0.000°
8C: Δx: 0.000, Δy: 0.000 rot: 0.000°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.000, Δy: 0.000 rot: 0.000°
11C: Δx: 0.000, Δy: 0.000 rot: 0.000°
12C: Δx: 0.000, Δy: 0.000 rot: 0.000°
13R: Δx: 0.000, Δy: 0.000 rot: 0.000°
14R: Δx: 0.000, Δy: 0.000 rot: 0.000°
15R: Δx: 0.000, Δy: 0.000 rot: 0.000°
16R: Δx: 0.000, Δy: 0.000 rot: 0.000°
17R: Δx: 0.000, Δy: 0.000 rot: 0.000°
18R: Δx: 0.000, Δy: 0.000 rot: 0.000°
print("\nContribution to the total cost of each module/position")
for m in range(1,19):
print(m, "{:10f}\t {:10f}\t {:10f}".format(*tuple(submodule_cost(guess64, m, i) for i in range(3))))
Contribution to the total cost of each module/position
1 29.334688 0.000000 0.000000
2 32.040963 0.000000 0.000000
3 6.232860 0.000000 0.000000
4 6.297670 0.000000 0.000000
5 2.009452 0.000000 0.000000
6 0.321601 43.713955 0.000000
7 6.175967 51.543986 0.000000
8 0.000000 25.054970 0.000000
9 0.000000 2.765519 0.000000
10 0.000000 22.727085 0.000000
11 0.000000 141.211026 0.000000
12 0.000000 166.945703 1.342367
13 0.000000 53.661477 20.621033
14 0.000000 0.000000 143.536133
15 0.000000 0.000000 231.760815
16 0.000000 0.000000 285.269525
17 0.000000 0.000000 661.884989
18 0.000000 0.000000 548.261372
Fit all modules of the central grid position:#
# Fit the center position:
def cost_all_center(param):
"""contains: step, y_min_1, x_min_1, angle_1, ...
returns the sum of distance squared in pixel space
"""
return sum(submodule_cost(param, module=i, position=1) for i in range(6,13))
print_res64(guess64)
print(cost_all_center(guess64))
res_center = minimize(cost_all_center, guess64, method = "slsqp")
print_res64(res_center.x)
print(res_center)
step: 29.303
6L: Δx: 1262.878, Δy: 6.559 rot: -0.020°
9C: Δx: 1984.210, Δy: 9.926 rot: -0.017°
12R: Δx: 2852.995, Δy: 8.558 rot: -0.035°
1L: Δx: 0.000, Δy: 0.000 rot: 0.000°
2L: Δx: 0.000, Δy: 0.000 rot: 0.000°
3L: Δx: 0.000, Δy: 0.000 rot: 0.000°
4L: Δx: 0.000, Δy: 0.000 rot: 0.000°
5L: Δx: 0.000, Δy: 0.000 rot: 0.000°
6C: Δx: 0.000, Δy: 0.000 rot: 0.000°
7C: Δx: 0.000, Δy: 0.000 rot: 0.000°
8C: Δx: 0.000, Δy: 0.000 rot: 0.000°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.000, Δy: 0.000 rot: 0.000°
11C: Δx: 0.000, Δy: 0.000 rot: 0.000°
12C: Δx: 0.000, Δy: 0.000 rot: 0.000°
13R: Δx: 0.000, Δy: 0.000 rot: 0.000°
14R: Δx: 0.000, Δy: 0.000 rot: 0.000°
15R: Δx: 0.000, Δy: 0.000 rot: 0.000°
16R: Δx: 0.000, Δy: 0.000 rot: 0.000°
17R: Δx: 0.000, Δy: 0.000 rot: 0.000°
18R: Δx: 0.000, Δy: 0.000 rot: 0.000°
453.9622451839362
step: 29.287
6L: Δx: 1262.878, Δy: 6.559 rot: -0.020°
9C: Δx: 1984.247, Δy: 9.963 rot: -0.018°
12R: Δx: 2852.995, Δy: 8.558 rot: -0.035°
1L: Δx: 0.000, Δy: 0.000 rot: 0.000°
2L: Δx: 0.000, Δy: 0.000 rot: 0.000°
3L: Δx: 0.000, Δy: 0.000 rot: 0.000°
4L: Δx: 0.000, Δy: 0.000 rot: 0.000°
5L: Δx: 0.000, Δy: 0.000 rot: 0.000°
6C: Δx: -1.052, Δy: -1.188 rot: -0.321°
7C: Δx: -0.463, Δy: -1.277 rot: 0.009°
8C: Δx: -0.136, Δy: -0.622 rot: -0.005°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.799, Δy: 0.326 rot: 0.005°
11C: Δx: 1.868, Δy: 0.654 rot: 0.057°
12C: Δx: 2.875, Δy: 0.965 rot: 0.092°
13R: Δx: 0.000, Δy: 0.000 rot: 0.000°
14R: Δx: 0.000, Δy: 0.000 rot: 0.000°
15R: Δx: 0.000, Δy: 0.000 rot: 0.000°
16R: Δx: 0.000, Δy: 0.000 rot: 0.000°
17R: Δx: 0.000, Δy: 0.000 rot: 0.000°
18R: Δx: 0.000, Δy: 0.000 rot: 0.000°
message: Optimization terminated successfully
success: True
status: 0
fun: 24.67610245679468
x: [ 2.929e+01 6.559e+00 ... 0.000e+00 0.000e+00]
nit: 28
jac: [-2.790e-03 0.000e+00 ... 0.000e+00 0.000e+00]
nfev: 1894
njev: 28
multipliers: []
Fit all modules of the left grid position:#
def cost_all_left(param):
"""contains: step, y_min_1, x_min_1, angle_1, ...
returns the sum of distance squared in pixel space
"""
return sum(submodule_cost(param, module=i, position=0) for i in range(1,6))
print_res64(res_center.x)
print(cost_all_left(res_center.x))
res_left = minimize(cost_all_left, res_center.x, method = "slsqp")
print_res64(res_left.x)
print(res_left)
step: 29.287
6L: Δx: 1262.878, Δy: 6.559 rot: -0.020°
9C: Δx: 1984.247, Δy: 9.963 rot: -0.018°
12R: Δx: 2852.995, Δy: 8.558 rot: -0.035°
1L: Δx: 0.000, Δy: 0.000 rot: 0.000°
2L: Δx: 0.000, Δy: 0.000 rot: 0.000°
3L: Δx: 0.000, Δy: 0.000 rot: 0.000°
4L: Δx: 0.000, Δy: 0.000 rot: 0.000°
5L: Δx: 0.000, Δy: 0.000 rot: 0.000°
6C: Δx: -1.052, Δy: -1.188 rot: -0.321°
7C: Δx: -0.463, Δy: -1.277 rot: 0.009°
8C: Δx: -0.136, Δy: -0.622 rot: -0.005°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.799, Δy: 0.326 rot: 0.005°
11C: Δx: 1.868, Δy: 0.654 rot: 0.057°
12C: Δx: 2.875, Δy: 0.965 rot: 0.092°
13R: Δx: 0.000, Δy: 0.000 rot: 0.000°
14R: Δx: 0.000, Δy: 0.000 rot: 0.000°
15R: Δx: 0.000, Δy: 0.000 rot: 0.000°
16R: Δx: 0.000, Δy: 0.000 rot: 0.000°
17R: Δx: 0.000, Δy: 0.000 rot: 0.000°
18R: Δx: 0.000, Δy: 0.000 rot: 0.000°
179.77361837563595
step: 29.317
6L: Δx: 1262.794, Δy: 6.840 rot: 0.018°
9C: Δx: 1984.247, Δy: 9.963 rot: -0.018°
12R: Δx: 2852.995, Δy: 8.558 rot: -0.035°
1L: Δx: 0.114, Δy: -0.276 rot: 0.037°
2L: Δx: -0.226, Δy: -0.023 rot: 0.004°
3L: Δx: -0.019, Δy: 0.320 rot: 0.042°
4L: Δx: -0.024, Δy: 0.241 rot: -0.041°
5L: Δx: 0.071, Δy: 0.019 rot: -0.021°
6C: Δx: -1.052, Δy: -1.188 rot: -0.321°
7C: Δx: -0.463, Δy: -1.277 rot: 0.009°
8C: Δx: -0.136, Δy: -0.622 rot: -0.005°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.799, Δy: 0.326 rot: 0.005°
11C: Δx: 1.868, Δy: 0.654 rot: 0.057°
12C: Δx: 2.875, Δy: 0.965 rot: 0.092°
13R: Δx: 0.000, Δy: 0.000 rot: 0.000°
14R: Δx: 0.000, Δy: 0.000 rot: 0.000°
15R: Δx: 0.000, Δy: 0.000 rot: 0.000°
16R: Δx: 0.000, Δy: 0.000 rot: 0.000°
17R: Δx: 0.000, Δy: 0.000 rot: 0.000°
18R: Δx: 0.000, Δy: 0.000 rot: 0.000°
message: Optimization terminated successfully
success: True
status: 0
fun: 2.134281668908724
x: [ 2.932e+01 6.840e+00 ... 0.000e+00 0.000e+00]
nit: 17
jac: [-3.866e-03 1.961e-03 ... 0.000e+00 0.000e+00]
nfev: 1169
njev: 17
multipliers: []
Fit all modules of the right grid position:#
def cost_all_right(param):
"""contains: step, y_min_1, x_min_1, angle_1, ...
returns the sum of distance squared in pixel space
"""
return sum(submodule_cost(param, module=i, position=2) for i in range(13,19))
bounds = [(None, None) for i in range(64)]
bounds[0] = (29, 30)
for i in range(3, 64, 3):
bounds[i] = (-.1, .1) #limit rotations
print_res64(res_left.x)
print(cost_all_right(res_left.x))
res_right = minimize(cost_all_right, res_left.x, method = "slsqp", bounds=bounds)
print_res64(res_right.x)
print(res_right)
step: 29.317
6L: Δx: 1262.794, Δy: 6.840 rot: 0.018°
9C: Δx: 1984.247, Δy: 9.963 rot: -0.018°
12R: Δx: 2852.995, Δy: 8.558 rot: -0.035°
1L: Δx: 0.114, Δy: -0.276 rot: 0.037°
2L: Δx: -0.226, Δy: -0.023 rot: 0.004°
3L: Δx: -0.019, Δy: 0.320 rot: 0.042°
4L: Δx: -0.024, Δy: 0.241 rot: -0.041°
5L: Δx: 0.071, Δy: 0.019 rot: -0.021°
6C: Δx: -1.052, Δy: -1.188 rot: -0.321°
7C: Δx: -0.463, Δy: -1.277 rot: 0.009°
8C: Δx: -0.136, Δy: -0.622 rot: -0.005°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.799, Δy: 0.326 rot: 0.005°
11C: Δx: 1.868, Δy: 0.654 rot: 0.057°
12C: Δx: 2.875, Δy: 0.965 rot: 0.092°
13R: Δx: 0.000, Δy: 0.000 rot: 0.000°
14R: Δx: 0.000, Δy: 0.000 rot: 0.000°
15R: Δx: 0.000, Δy: 0.000 rot: 0.000°
16R: Δx: 0.000, Δy: 0.000 rot: 0.000°
17R: Δx: 0.000, Δy: 0.000 rot: 0.000°
18R: Δx: 0.000, Δy: 0.000 rot: 0.000°
1712.6937399556489
step: 29.317
6L: Δx: 1262.794, Δy: 6.840 rot: 0.018°
9C: Δx: 1984.247, Δy: 9.963 rot: -0.018°
12R: Δx: 2853.237, Δy: 11.652 rot: -0.110°
1L: Δx: 0.114, Δy: -0.276 rot: 0.037°
2L: Δx: -0.226, Δy: -0.023 rot: 0.004°
3L: Δx: -0.019, Δy: 0.320 rot: 0.042°
4L: Δx: -0.024, Δy: 0.241 rot: -0.041°
5L: Δx: 0.071, Δy: 0.019 rot: -0.021°
6C: Δx: -1.052, Δy: -1.188 rot: -0.321°
7C: Δx: -0.463, Δy: -1.277 rot: 0.009°
8C: Δx: -0.136, Δy: -0.622 rot: -0.005°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.799, Δy: 0.326 rot: 0.005°
11C: Δx: 1.868, Δy: 0.654 rot: 0.057°
12C: Δx: 2.875, Δy: 0.965 rot: 0.092°
13R: Δx: -0.697, Δy: -2.076 rot: 0.250°
14R: Δx: -0.561, Δy: -0.791 rot: 0.203°
15R: Δx: -0.071, Δy: 0.171 rot: 0.207°
16R: Δx: 0.303, Δy: 1.036 rot: 0.173°
17R: Δx: 0.480, Δy: 1.975 rot: 0.230°
18R: Δx: 0.789, Δy: 2.782 rot: 0.239°
message: Optimization terminated successfully
success: True
status: 0
fun: 6.861655272965805
x: [ 2.932e+01 6.840e+00 ... 7.889e-01 4.169e-03]
nit: 26
jac: [ 1.176e+01 0.000e+00 ... 6.734e-02 -7.742e-01]
nfev: 1764
njev: 26
multipliers: []
Fit all modules together#
def cost_all(param):
"""contains: step, y_min_1, x_min_1, angle_1, ...
returns the sum of distance squared in pixel space
"""
return (sum(submodule_cost(param, module=i, position=0) for i in range(1, 19))+
sum(submodule_cost(param, module=i, position=1) for i in range(1, 19))+
sum(submodule_cost(param, module=i, position=2) for i in range(1, 19)))
print_res64(res_right.x)
print(cost_all(res_right.x))
res_all = minimize(cost_all, res_right.x, method = "slsqp")
print_res64(res_all.x)
print(res_all)
step: 29.317
6L: Δx: 1262.794, Δy: 6.840 rot: 0.018°
9C: Δx: 1984.247, Δy: 9.963 rot: -0.018°
12R: Δx: 2853.237, Δy: 11.652 rot: -0.110°
1L: Δx: 0.114, Δy: -0.276 rot: 0.037°
2L: Δx: -0.226, Δy: -0.023 rot: 0.004°
3L: Δx: -0.019, Δy: 0.320 rot: 0.042°
4L: Δx: -0.024, Δy: 0.241 rot: -0.041°
5L: Δx: 0.071, Δy: 0.019 rot: -0.021°
6C: Δx: -1.052, Δy: -1.188 rot: -0.321°
7C: Δx: -0.463, Δy: -1.277 rot: 0.009°
8C: Δx: -0.136, Δy: -0.622 rot: -0.005°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.799, Δy: 0.326 rot: 0.005°
11C: Δx: 1.868, Δy: 0.654 rot: 0.057°
12C: Δx: 2.875, Δy: 0.965 rot: 0.092°
13R: Δx: -0.697, Δy: -2.076 rot: 0.250°
14R: Δx: -0.561, Δy: -0.791 rot: 0.203°
15R: Δx: -0.071, Δy: 0.171 rot: 0.207°
16R: Δx: 0.303, Δy: 1.036 rot: 0.173°
17R: Δx: 0.480, Δy: 1.975 rot: 0.230°
18R: Δx: 0.789, Δy: 2.782 rot: 0.239°
509.9063278735787
step: 29.337
6L: Δx: 1262.789, Δy: 6.487 rot: 0.063°
9C: Δx: 1984.271, Δy: 9.749 rot: 0.055°
12R: Δx: 2852.538, Δy: 8.311 rot: 0.002°
1L: Δx: 0.896, Δy: 0.980 rot: -0.008°
2L: Δx: 0.387, Δy: 1.044 rot: -0.040°
3L: Δx: 0.438, Δy: 1.190 rot: -0.003°
4L: Δx: 0.243, Δy: 0.921 rot: -0.086°
5L: Δx: 0.193, Δy: 0.503 rot: -0.066°
6C: Δx: -0.147, Δy: -0.181 rot: -0.393°
7C: Δx: 0.254, Δy: -0.502 rot: -0.074°
8C: Δx: 0.066, Δy: -0.241 rot: -0.077°
9C: Δx: 0.000, Δy: 0.000 rot: 0.000°
10C: Δx: 0.130, Δy: 0.079 rot: -0.067°
11C: Δx: 0.815, Δy: 0.091 rot: -0.016°
12C: Δx: 1.395, Δy: 0.094 rot: 0.019°
13R: Δx: 0.129, Δy: 0.687 rot: 0.153°
14R: Δx: -0.189, Δy: 1.520 rot: 0.090°
15R: Δx: 0.147, Δy: 1.991 rot: 0.095°
16R: Δx: 0.326, Δy: 2.380 rot: 0.061°
17R: Δx: 0.349, Δy: 2.828 rot: 0.118°
18R: Δx: 0.503, Δy: 3.158 rot: 0.127°
message: Optimization terminated successfully
success: True
status: 0
fun: 64.36184904794223
x: [ 2.934e+01 6.487e+00 ... 5.033e-01 2.209e-03]
nit: 68
jac: [ 1.677e-03 1.144e-05 ... -1.907e-06 9.537e-07]
nfev: 4634
njev: 68
multipliers: []
print("\nContribution to the total cost of each module/position")
for m in range(1,19):
print(m, "{:10f}\t {:10f}\t {:10f}".format(*tuple(submodule_cost(res_all.x, m, i) for i in range(3))))
Contribution to the total cost of each module/position
1 1.513509 0.000000 0.000000
2 1.051964 0.000000 0.000000
3 0.096768 0.000000 0.000000
4 0.081735 0.000000 0.000000
5 0.204421 0.000000 0.000000
6 1.665109 2.111053 0.000000
7 1.316641 6.960473 0.000000
8 0.000000 7.142645 0.000000
9 0.000000 5.203996 0.000000
10 0.000000 3.296920 0.000000
11 0.000000 4.494788 0.000000
12 0.000000 3.533906 4.087876
13 0.000000 11.619559 3.759833
14 0.000000 0.000000 0.978354
15 0.000000 0.000000 0.433243
16 0.000000 0.000000 0.549202
17 0.000000 0.000000 2.080245
18 0.000000 0.000000 2.179608
Reconstruction of the pixel position#
The pixel position can be obtained from the standard Pilatus detector. Each module is then displaced according to the fitted values, except the first one which is left where it is.
pixel_coord = pyFAI.detector_factory("Pilatus900kwCdTe").get_pixel_corners()
pixel_coord_raw = pixel_coord.copy()
def correct(x, y, dx, dy, angle, module):
"apply the correction dx, dy and angle to those pixels ..."
trans = numpy.array([[dx],
[dy]])
xy_guess = numpy.vstack((x.ravel(),
y.ravel()))
xy_cor = rotate(-angle, xy_guess, module) - trans
xy_cor = xy_cor.reshape((2,)+x.shape)
return xy_cor[0], xy_cor[1]
for module in range(1, 19):
# Extract the pixel corners for one module
module_idx = numpy.where(mid == module)
one_module = pixel_coord_raw[module_idx]
#retrieve the fitted values
dy, dx, angle = res_all.x[7+3*module: 10+3*module]
#z = one_module[...,0]
y = one_module[...,1]/pilatus.pixel1
x = one_module[...,2]/pilatus.pixel2
x_cor, y_cor = correct(x, y, dx, dy, angle, module)
if i>12:
dy, dx, angle = res_all.x[7+3*12: 10+3*12]
x_cor, y_cor = correct(x_cor, y_cor, dx, dy, angle, 12)
if i<6:
dy, dx, angle = res_all.x[7+3*6: 10+3*6]
x_cor, y_cor = correct(x_cor, y_cor, dx, dy, angle, 6)
one_module[...,1] = y_cor * pilatus.pixel1
one_module[...,2] = x_cor * pilatus.pixel2
#Update the array
pixel_coord_raw[module_idx] = one_module
pilatus.set_pixel_corners(pixel_coord_raw)
pilatus.mask = mask0 | mask1 | mask2
pilatus.save("Pilatus_ID06_raw.h5")
displ = numpy.sqrt(((pixel_coord - pixel_coord_raw)**2).sum(axis=-1))
displ /= pilatus.pixel1 #convert in pixel units
fig, ax = subplots(figsize=(8,6))
ax.hist(displ.ravel(), 100)
ax.set_title("Displacement of pixels versus the reference representation")
ax.set_xlabel("Error in pixel size (172µm)");
#Kabsch alignment of the whole detector ...
unmasked = numpy.logical_not(all_masks)
misaligned = numpy.vstack((pixel_coord_raw[..., 2].ravel(), #x
pixel_coord_raw[..., 1].ravel())) #y
reference = numpy.vstack((pixel_coord[..., 2].ravel(), #x
pixel_coord[..., 1].ravel())) #y
def kabsch(P, R):
"Align P on R"
centroid_P = P.mean(axis=0)
centroid_R = R.mean(axis=0)
centered_P = P - centroid_P
centered_R = R - centroid_R
C = numpy.dot(centered_P.T, centered_R)
V, S, W = numpy.linalg.svd(C)
d = (numpy.linalg.det(V) * numpy.linalg.det(W)) < 0.0
if d:
S[-1] = -S[-1]
V[:, -1] = -V[:, -1]
# Create Rotation matrix U
U = numpy.dot(V, W)
P = numpy.dot(centered_P, U)
return P + centroid_R
%time aligned = kabsch(misaligned.T, reference.T).T
CPU times: user 3.56 s, sys: 13.8 ms, total: 3.58 s
Wall time: 132 ms
displ = numpy.sqrt(((aligned-reference)**2).sum(axis=0))
displ /= pilatus.pixel1 #convert in pixel units
fig, ax = subplots(figsize=(8,6))
ax.hist(displ.ravel(), 100)
ax.set_title("Displacement of pixels versus the reference representation")
ax.set_xlabel("Pixel size (172µm)");
pixel_coord_aligned = pixel_coord.copy()
pixel_coord_aligned[...,1] = aligned[1,:].reshape(pixel_coord.shape[:-1])
pixel_coord_aligned[...,2] = aligned[0,:].reshape(pixel_coord.shape[:-1])
pilatus.set_pixel_corners(pixel_coord_aligned)
pilatus.mask = all_masks
pilatus.save("Pilatus_ID06_final.h5")
fig, ax = subplots(2, figsize=(20, 4))
i0 = ax[0].imshow((pixel_coord_aligned[...,2].mean(axis=-1) - pixel_coord[...,2].mean(axis=-1))/pilatus.pixel2)
ax[0].set_title("Displacement x (in pixel)")
i1 = ax[1].imshow((pixel_coord_aligned[...,1].mean(axis=-1) - pixel_coord[...,1].mean(axis=-1))/pilatus.pixel1)
ax[1].set_title("Displacement y (in pixel)")
fig.colorbar(i0, ax=ax[0])
fig.colorbar(i1, ax=ax[1]);
Conclusion#
This tutorial presents the way to calibrate a large module based detector using a small grid. The HDF5 file generated is directly useable by any parts of pyFAI, the reader is invited in calibrating the rings images with the default definition and with this optimized definition and check the residual error is almost divided by a factor two.
To come back on the precision of the localization of the pixel: not all the pixel are within the specifications provided by Dectris which claims the misalignment of the modules is within one pixel.
Nota: There is not validation yet of this modelization of the detector. There has been no parallax effect corrections so far.
print(f"Total execution time: {time.perf_counter() - start_time:.3f} s")
Total execution time: 42.193 s