Skip to content

Analysis of PyFAI geometry optimization #375

Description

@LouConreux

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Å

1D_integration_init

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:
cp_extract_test1
1D_integration_test1

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Å

cp_extract_test2
1D_integration_test2

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Å

cp_extract_test3
1D_integration_test3

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Å

cp_extract_test4
1D_integration_test4

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Å

cp_extract_test5
1D_integration_test5

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Å

1D_integration_ground_truth

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions