Analysis of PyFAI geometry optimization
Context
Following the geometry calibration on experiment mfxx49820 run 8 (see #197), we would like to translate the custom-made geometry optimization with a more-globally used library pyFAI. Using pyFAI would allow more robustness and rapidity in finding the optimal geometry of an experiment.
Geometry format
LCLS is using the CrystFEL format, so first one need to parse the CrystFEL geometry in a format that PyFAI can digest. Then optimizing through PyFAI reduces to finding the optimal 6 parameters defining the coordinates of the detector reference frame compared to the origin being the sample position (3 translations and 3 rotations). See https://www.silx.org/doc/pyFAI/dev/geometry.html#geometry for more details. In the following, we will then try to optimize the geometry by playing on the 3 translation parameters: poni1, poni2 and dist (PONI standing for "Point of Normal Incidence" = the orthogonal projection of the sample position onto the detector plane) (We make the assumption that we are in a transmission setting meaning all rotations are 0).
Optimizing the Geometry
Ground Truth
To set the ground truth for making sure PyFAI finds the right geometry, we refer to the analysis of #197 where the center (in pixels) of the geometry was found to be
New center is (843.5532519182825, 831.0710902924419) \\
Detector distance inferred from powder rings: 280.47 mm
with all rotations being 0.
In PyFAI reference frame, this coordinate corresponds to approximately (x=0.5, y=5)
Optimization
PyFAI struggles to find the optimal geometry with only one shot. So, we will apply the optimization recursively until convergence.
Initialization
Geometry Format Conversion
First, need to convert the CrystFEL format into a PyFAI-friendly format:
geom_path = '/sdf/data/lcls/ds/mfx/mfxx49820/scratch/dorlhiac/geom/r0008.geom'
converter = CrystFEL_to_PyFAI(geom_path)
epix10k2M = converter.detector
epix10k2M.set_pixel_corners(converter.corner_array)
Load Data
Load the calibrated images:
exp = 'mfxx49820'
run = 8
detector = 'epix10k2M'
psi = PsanaInterface(exp=exp, run=run, det_type=detector)
print(f'Instantiated run {run} of exp {exp} looking at {detector}')
print("Raw Images")
psi.calibrate = True
raw_images = psi.get_images(100, assemble=False)
print(f'Images shape: {raw_images.shape}')
calib_avg = np.mean(raw_images, axis=0)
calib_avg_flat = np.reshape(calib_avg, (16*2*176,2*192))
Here, we need to reshape the calib_avg (in (Np, Nx, Ny)) to the shape supported by PyFAI (Np*Nx, Ny)
Set Experiment Initial Conditions
Define the guessed dist, poni1, poni2 and the wavelength used.
wavelength = psi.get_wavelength() * 1e-10
dist = 0.4
p1, p2, p3 = epix10k2M.calc_cartesian_positions()
poni1 = +p1.mean()*2
poni2 = +p2.mean()*4
Calling the Geometry fitting
from pyFAI.azimuthalIntegrator import AzimuthalIntegrator
from pyFAI.gui import jupyter
ai = AzimuthalIntegrator(dist=dist, detector=epix10k2M, wavelength=wavelength, poni1=poni1, poni2=poni2)
print(ai)
res = ai.integrate1d(calib_avg_flat, 1000)
ax = jupyter.plot1d(res)
Here, we voluntarily defined parameters to be way off the true center. We get:
Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 4.000000e-01 m PONI= -1.452228e-03, -5.401834e-04 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 400.000 mm Center: x=-5.402, y=-14.522 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å

Instantiating the PyFAI Calibrant
Here, this was Silver Behenate:
from pyFAI.calibrant import CalibrantFactory, CALIBRANT_FACTORY
behenate = CALIBRANT_FACTORY("AgBh")
behenate.wavelength = wavelength
Optimization Loop
Below is what the optimization looks like. First, it instantiated the guessed geometry. Then it passes through a SingleGeometry object that extracts control points between raw data and calibrant data. Using those control points, a new fitting is finally set and we repeat until convergence.
from pyFAI.goniometer import SingleGeometry
geom_initial = pyFAI.geometry.Geometry(dist=dist, poni1=poni1, poni2=poni2, detector=epix10k2M, wavelength=wavelength)
# The SingleGeometry object (from goniometer) allows to extract automatically ring and calibrate
sg = SingleGeometry("test1", calib_avg_flat, calibrant=behenate, detector=epix10k2M, geometry=geom_initial)
sg.extract_cp(max_rings=5, pts_per_deg=0.2, Imin=3)
# We fix rot1 = rot2 = rot3 = 0 and wavelength should not be changed
sg.geometry_refinement.refine3(fix=["rot1", "rot2", "rot3", "wavelength"])
sg.geometry_refinement.param
geom_initial = pyFAI.geometry.Geometry(dist=sg.geometry_refinement.param[0], poni1=sg.geometry_refinement.param[1], poni2=sg.geometry_refinement.param[2], detector=epix10k2M, wavelength=wavelength)
Results
For n=5 loops, we obtained the following convergence:


Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 4.169748e-01 m PONI= -1.212360e-03, -5.071352e-04 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 416.975 mm Center: x=-5.071, y=-12.124 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å


Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 4.210260e-01 m PONI= -8.301785e-04, -3.989296e-04 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 421.026 mm Center: x=-3.989, y=-8.302 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å


Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 4.217013e-01 m PONI= 4.613455e-05, -3.942903e-05 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 421.701 mm Center: x=-0.394, y=0.461 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å


Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 4.214463e-01 m PONI= 3.905332e-04, 2.993149e-05 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 421.446 mm Center: x=0.299, y=3.905 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å


Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 4.218728e-01 m PONI= 4.825142e-04, 5.180493e-05 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 421.873 mm Center: x=0.518, y=4.825 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å
This pretty good for a start, not so far away from the ground truth center. However, PyFAI fails to find the best sample-detector distance which is dist=280mm. We can see that the more loops we perform, the more control points fit right the rings, which helps reaching convergence.
Detector Epix10k2M PixelSize= 1.000e-04, 1.000e-04 m BottomRight (3)
Wavelength= 1.290991e-10 m
SampleDetDist= 2.800000e-01 m PONI= 5.000000e-04, 5.000000e-05 m rot1=0.000000 rot2=0.000000 rot3=0.000000 rad
DirectBeamDist= 280.000 mm Center: x=0.500, y=5.000 pix Tilt= 0.000° tiltPlanRotation= 0.000° 𝛌= 1.291Å

Next Steps
Finding why dist cannot be optimized ?
Finding best hyperparameters for extract_cp
In this optimization, the most crucial function is extract_cp which automatically extract control points on rings.
We can play on the number of points we want on 3 hyperparameters:
max_rings selects the furthest rings where to extract control points
pts_per_deg selects the "spacing" or "resolution" between two extracted control points
Imin specifies a minimum intensity for which points below that intensity cannot be extracted
Testing robustness
Playing with different initialization conditions, setting the guessed geometry way way too far from the ground truth may result in divergence (maybe too few control points).
Analysis of PyFAI geometry optimization
Context
Following the geometry calibration on experiment mfxx49820 run 8 (see #197), we would like to translate the custom-made geometry optimization with a more-globally used library pyFAI. Using pyFAI would allow more robustness and rapidity in finding the optimal geometry of an experiment.
Geometry format
LCLS is using the CrystFEL format, so first one need to parse the CrystFEL geometry in a format that PyFAI can digest. Then optimizing through PyFAI reduces to finding the optimal 6 parameters defining the coordinates of the detector reference frame compared to the origin being the sample position (3 translations and 3 rotations). See https://www.silx.org/doc/pyFAI/dev/geometry.html#geometry for more details. In the following, we will then try to optimize the geometry by playing on the 3 translation parameters: poni1, poni2 and dist (PONI standing for "Point of Normal Incidence" = the orthogonal projection of the sample position onto the detector plane) (We make the assumption that we are in a transmission setting meaning all rotations are 0).
Optimizing the Geometry
Ground Truth
To set the ground truth for making sure PyFAI finds the right geometry, we refer to the analysis of #197 where the center (in pixels) of the geometry was found to be
with all rotations being 0.
In PyFAI reference frame, this coordinate corresponds to approximately (x=0.5, y=5)
Optimization
PyFAI struggles to find the optimal geometry with only one shot. So, we will apply the optimization recursively until convergence.
Initialization
Geometry Format Conversion
First, need to convert the CrystFEL format into a PyFAI-friendly format:
Load Data
Load the calibrated images:
Here, we need to reshape the calib_avg (in (Np, Nx, Ny)) to the shape supported by PyFAI (Np*Nx, Ny)
Set Experiment Initial Conditions
Define the guessed dist, poni1, poni2 and the wavelength used.
Calling the Geometry fitting
Here, we voluntarily defined parameters to be way off the true center. We get:
Instantiating the PyFAI Calibrant
Here, this was Silver Behenate:
Optimization Loop
Below is what the optimization looks like. First, it instantiated the guessed geometry. Then it passes through a SingleGeometry object that extracts control points between raw data and calibrant data. Using those control points, a new fitting is finally set and we repeat until convergence.
Results
For n=5 loops, we obtained the following convergence:


This pretty good for a start, not so far away from the ground truth center. However, PyFAI fails to find the best sample-detector distance which is dist=280mm. We can see that the more loops we perform, the more control points fit right the rings, which helps reaching convergence.
Next Steps
Finding why dist cannot be optimized ?
Finding best hyperparameters for extract_cp
In this optimization, the most crucial function is extract_cp which automatically extract control points on rings.
We can play on the number of points we want on 3 hyperparameters:
max_rings selects the furthest rings where to extract control points
pts_per_deg selects the "spacing" or "resolution" between two extracted control points
Imin specifies a minimum intensity for which points below that intensity cannot be extracted
Testing robustness
Playing with different initialization conditions, setting the guessed geometry way way too far from the ground truth may result in divergence (maybe too few control points).