pyFAI icon indicating copy to clipboard operation
pyFAI copied to clipboard

Incorrect calculation of exit angle for Q vector

Open P-Mousley opened this issue 1 year ago • 18 comments

I think that the calculation performed when requesting Q units for 2d map calculations appears to be only valid when x=0, which does not work if you want to move the detector using rot2. The function eq_exitangle that has the issue is here

the current version is simply

numpy.arctan2(y, z) - incident_angle

which does not take into account the effect of x, and also assumes that the exit vector is aligned with the incident angle

a possible correction is as follows:


def eq_exitangle(x, y, z, wavelength=None, incident_angle=0.0, tilt_angle=0.0, sample_orientation=1):
    """Calculates the vertical exit scattering angle (relative to incident angle), used for GI/Fiber diffraction
    :param x: horizontal position, towards the center of the ring, from sample position
    :param y: vertical position, to the roof, from sample position
    :param z: distance from sample along the beam
    :param wavelength: in meter
    :return: exit angle for the position xyz in radians
    """
    #add in correction to do proper angle calculation - first need to get vector length]
    #then find height in y to use sin rule for angle to the y=0 plane
    fulllength=numpy.linalg.norm((x,y,z))
    #also need proper calculation of adjustment to height dependent on tilt_angle
    th1=numpy.arctan(z/y)
    height=y*(numpy.cos(th1+tilt_angle)/numpy.cos(th1))
    #use new values to calulate the radian angle to the xz plane at y=0
    rad_to_xz=numpy.arcsin(height/fulllength)
    return rad_to_xz

Let me know if i have understood the function correctly, and if the proposed correction is suitable.

P-Mousley avatar Oct 03 '24 14:10 P-Mousley

Hi Philip, Thanks for the report, I will let Edgar deal with this since I am not the most competent on this topic. Cheers, Jerome

kif avatar Oct 03 '24 18:10 kif

Hi, Philip. Thanks a lot for the issue. Indeed, there is an inconsistency with the exit_angle equation. However, to be consistent with the way I build the q equations, the correct equation is:

 numpy.arctan2(y, numpy.sqrt(z ** 2 + x ** 2))

I use this exit_angle before applying the incident_angle and tilt_angle rotation matrix, so this exit_angle is a magnitude relative to the direct beam axis: it's the angle between kf and kf_xy image

image

So, you're right that it depends on x, but it's a magnitude of the pixel position (only dependent of the poni and independent of the sample rotations). Again, this is my 'exit_angle' that I define like this, and then I apply the two rotation matrix to transform the q vector into the sample reference.

These days, I will include these corrections, also I will clarify the API with the equations, which are quite bulky.

image

EdgarGF93 avatar Oct 06 '24 16:10 EdgarGF93

#2297

EdgarGF93 avatar Oct 06 '24 16:10 EdgarGF93

Although I understand that the grazing incidence community calls exit_angle a magnitude relative to the horizon of the thin film sample, so it's a magnitude of the sample reference frame that, of course, depends on both incident_angle and tilt_angle. For my approach, what I call now vertical and horizontal exit angles are actually the components of the classic scattering_angle (2theta) from the powder diffraction point of view, which is I think the more "pyFAInic" way to implement this. It would be happy anyway to include this grazing_incidence exit_angle unit in the API.

EdgarGF93 avatar Oct 07 '24 09:10 EdgarGF93

Thank you for the explanation - I think the new vector component calculations make sense to me. If i have understood correctly the rot1,rot2,rot3 from the PONI file are used to update the XYZ in the labframe for the exit vector using equations defined in pyFAI/src/pyFAI/ext/_geometry.pyx . The updated XYZ is then passed to your updated equations in units.py to calculated the Q vector components, taking account of incident_angle and tilt_angle

i still have two questions:

  1. Am i correct in assuming this mean that to provide the details for incident_angle and tilt_angle you can no longer use
unit_qip = "qip_nm^-1"
unit_qoop = "qoop_nm^-1"

instead you need to use the grazing incidence fiber units e.g.

unit_qip = get_unit_fiber(name="qip_nm^-1", incident_angle=0.2, tilt_angle=0.4, sample_orientation=1)
unit_qoop = get_unit_fiber(name="qoop_nm^-1", incident_angle=0.2, tilt_angle=0.4, sample_orientation=1)
  1. Is there a specific reason why rotation 1 (around the axis towards the celing) needs to be done before rotation 2 (around the axis pointing towards the ring)? Because this makes it difficult to simulate a diffractometer where the out of plane angle (delta) is stacked ontop of the inplane angle (gamma). I think switching the order of how the angles are applied within the conversion equations would make it easier to translate from a (gamma, dellta) value, to a (rot1,rot2) value. However i am not sure if this would have larger consequences for the pyFAI codebase.

P-Mousley avatar Oct 08 '24 10:10 P-Mousley

Hi Philip, I am not a fiber guy so I can only answer for your second point. I wanted the last rotation to be free along Debye-Scherrer rings. There was no specific reason for the order of the rotation around vertical and horizontal axis of the detector and honestly, when I started pyFAI, I did not consider it would ever be used in conjunction with a diffractometer since everybody was happy with a detector mounted normal to the beam. Now pyFAI is used by thousands of users and PONI-files are distributed to all our users. Changing something so central is just not an option. But I can try to help in the math/trigonometry for converting geometries ... and this is something we would like to address for pyFAI2 (pluggable geometries)

kif avatar Oct 08 '24 14:10 kif

#2308

EdgarGF93 avatar Oct 18 '24 10:10 EdgarGF93

Hello, I have had a look at the conversion equations to get a rot1,rot2 value pair for a set value of gamma and delta. However it seems impossible to get a direct conversion because pyFAI applies the same rot1 and rot2 values to the whole image. When comparing the two methods of positioning the detector (either rot1 then rot2, or delta then gamma) there is always a mismatch with the image vectors even if the central pixel overlaps perfectly. the attached plot shows this mismatch:

  • the blue square the detector at all angels at 0
  • the orange square for rotation of delta then gamma
  • the green square for rotation of rot1 then rot2.

Screencast from 29-10-24 15:41:43.webm

Do you have any more details of what other users have been doing if they want to use pyFAI for images taken with diffractometers where the delta motion is stacked ontop of the gamma motion?

P-Mousley avatar Oct 29 '24 15:10 P-Mousley

I believe that if the two rotation are actually inverted (rot1+rot2 vs gamma+delta), the solution is to use an Euler transformation library like the one from Christoph Gohlke which is able to match any of many set of Euler angle. https://pypi.org/project/transformations/ There is a snapshot of this library in pyFAI.third_party.transformations which should be usable for this purpose.

kif avatar Oct 29 '24 21:10 kif

Hello @P-Mousley , could you share with us how you calculate coordinates in your script?

Hello, I have had a look at the conversion equations to get a rot1,rot2 value pair for a set value of gamma and delta. However it seems impossible to get a direct conversion because pyFAI applies the same rot1 and rot2 values to the whole image. When comparing the two methods of positioning the detector (either rot1 then rot2, or delta then gamma) there is always a mismatch with the image vectors even if the central pixel overlaps perfectly. the attached plot shows this mismatch:

* the blue square the detector at all angels at 0

* the orange square for rotation of delta then gamma

* the green square for rotation of rot1 then rot2.

Screencast.from.29-10-24.15.41.43.webm

Do you have any more details of what other users have been doing if they want to use pyFAI for images taken with diffractometers where the delta motion is stacked ontop of the gamma motion?

mjdiff avatar Oct 30 '24 10:10 mjdiff

Hello @P-Mousley , could you share with us how you calculate coordinates in your script?

Hello, I have had a look at the conversion equations to get a rot1,rot2 value pair for a set value of gamma and delta. However it seems impossible to get a direct conversion because pyFAI applies the same rot1 and rot2 values to the whole image. When comparing the two methods of positioning the detector (either rot1 then rot2, or delta then gamma) there is always a mismatch with the image vectors even if the central pixel overlaps perfectly. the attached plot shows this mismatch:

* the blue square the detector at all angels at 0

* the orange square for rotation of delta then gamma

* the green square for rotation of rot1 then rot2.

Screencast.from.29-10-24.15.41.43.webm Do you have any more details of what other users have been doing if they want to use pyFAI for images taken with diffractometers where the delta motion is stacked ontop of the gamma motion?

Hello @mjdiff ,

To calculate the co-ordinates in the script I used simple rotation matrices from scipy around the x,y,or z axis.

Expand code used for rotation calculation
import matplotlib.pyplot as plt
from scipy.spatial.transform import Rotation as R
import transformations as tf
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as wg
%matplotlib qt
ax = plt.axes([0,0,0.95,0.95],projection ='3d')


def drawline(vec1,vec2,axs,col):
    axs.plot([vec1[0],vec2[0]],[vec1[1],vec2[1]],[vec1[2],vec2[2]],color = col)

def drawdet(veclist,ax,col):
    drawline(veclist[0],veclist[1],ax,col)
    drawline(veclist[1],veclist[2],ax,col)
    drawline(veclist[2],veclist[3],ax,col)
    drawline(veclist[3],veclist[0],ax,col)
    drawline([0,0,0],veclist[4],ax,col)

def drawplots(gamma,delta,rot1,rot2,rot3):
    ax.cla()
    # Set the axis labels
    ax.set_xlabel('z')
    ax.set_ylabel('x')
    ax.set_zlabel('y')
    rotdelta=R.from_euler('y', -delta, degrees=True)
    rotgamma=R.from_euler('z',gamma,degrees=True)
    rot1mat=R.from_euler('y', -rot2, degrees=True)
    rot2mat=R.from_euler('z',rot1,degrees=True)
    rot3mat=R.from_euler('x',rot3,degrees=True)

    veclist=[[4,0.5,0.5],[4,0.5,-0.5],[4,-0.5,-0.5],[4,-0.5,0.5],[4,0,0]]
    totalrot=rotgamma*rotdelta
    totalrot2=rot3mat*rot2mat*rot1mat
    
    rotvecs1=[]
    rotvecs2=[]
    for vec in veclist:
        rotvecs1.append(totalrot.apply(vec))
        rotvecs2.append(totalrot2.apply(vec))


    drawdet(veclist,ax,'blue')
    drawdet(rotvecs1,ax,'orange')
    drawdet(rotvecs2,ax,'green')
    
    ax.set_zlim(-4,4)
    ax.set_ylim(-4,4)
    ax.set_xlim(-4,4)
gammaslide=wg.FloatSlider(min=-10, max=90, step=0.001, value=0)
deltaslide=wg.FloatSlider(min=-10, max=90, step=0.001, value=0)
rot1slide=wg.FloatSlider(min=-90, max=90, step=0.001, value=0)
rot2slide=wg.FloatSlider(min=-90, max=90, step=0.001, value=0)
rot3slide=wg.FloatSlider(min=-90, max=90, step=0.001, value=0)
interact(drawplots,axs=ax,gamma=gammaslide,delta=deltaslide,rot1=rot1slide,rot2=rot2slide,rot3=rot3slide)

The transformations library @kif recommend earlier has allowed me to calculate the corresponding values for rot1,rot2,rot3 which are equivalent to a delta +gamma pair of diffractometer angles - see code example below.

Expand code used for converting delta,gamma into rot1,rot2,rot3
    def gamdel2rots(self,gamma,delta):
        """
        

        Parameters
        ----------
        gamma : float
            angle rotation of gamma diffractometer circle in degrees.
        delta : float
            angle rotation of delta diffractometer circle in degrees.

        Returns
        -------
        rots : list of rotations rot1,rot2,rot3 in radians to be using by pyFAI.

        """
        rotdelta=R.from_euler('y', -delta, degrees=True)
        rotgamma=R.from_euler('z',gamma,degrees=True)
        totalrot=rotgamma*rotdelta
        fullrot=np.identity(4)
        fullrot[0:3,0:3]=totalrot.as_matrix()
        vals=tf.euler_from_matrix(fullrot,'rxyz')
        rots=vals[2],-vals[1],vals[0]
        return rots

P-Mousley avatar Oct 30 '24 17:10 P-Mousley

Great ... Thanks for your work. Can we reuse it in the documentation ?

kif avatar Oct 30 '24 20:10 kif

Great ... Thanks for your work. Can we reuse it in the documentation ?

Yes, happy for you to use my code

P-Mousley avatar Oct 31 '24 07:10 P-Mousley

Hello All. So, is this issue closed now? Was it corrected?

Anas321 avatar May 09 '25 01:05 Anas321

I believe this is mostly integrated in the latest version of pyFAI but maybe some documentation remains to be updated, thus the unclosed issue. Edgar could comment on it but he is currently off.

kif avatar May 09 '25 08:05 kif

@kif Ok, thanks so much for your response.

I also have another, different, question. If I want to look at the equations used to construct the q vector, is this the right place to look in: https://github.com/silx-kit/pyFAI/blob/main/src/pyFAI/units.py

I am trying to compare what I am getting from pyFAI with my manual calculations using equation (1) in Hexemer's paper: https://journals.iucr.org/m/issues/2015/01/00/ed5003/ed5003.pdf

However, I am finding some discrepancies in the vertical direction of the q vector, and I just want to confirm that I am looking at the right place in pyFAI's source code. I am still investigating this. There might be a mistake on my side.

Appreciate your help. Thanks!

Anas321 avatar May 09 '25 18:05 Anas321

Units.py is the right place to look for. I will also read the paper and check ... but I am not a specialist of surface diffraction.

kif avatar May 09 '25 19:05 kif

@kif Thanks so much for your help and prompt response.

Anas321 avatar May 09 '25 19:05 Anas321