Description
Hi,
This is not really an issue, but something I have been working on and thought would be productive to share. Did not know if this is too specific to be included in Orix in any way, but might be useful as a tutorial to highlight how orientations may be combined.
The main idea here is to find the x- and y-tilt of the double tilt gonio system in a TEM, which orients the crystal to be in a chosen zone axis. To solve this problem, the orientation,
The rotation matrix which describes the rotation of the double tilt holder in a TEM, when the x-tilt is changed from
The product
Thus, to meaningfully transform the orientation
where the first inversion of
To use the transformation above to find what sample tilt leaves the crystal in a desired zone axis, one can simply search for values of
Below, I have included some code implementing the procedure. From my testing, the approach appears to work well, and the predicted x- and y-tilt is usually within 1° of the actual zone axis.
import numpy as np
from orix.quaternion import Rotation, Orientation
from orix.vector import Miller
from scipy.optimize import minimize
class GonioPosition:
"""This class represents a sample tilt position"""
def __init__(self, x_tilt, y_tilt, degrees=True) -> None:
if degrees:
self.x_tilt = np.radians(x_tilt)
self.y_tilt = np.radians(y_tilt)
else:
self.x_tilt = x_tilt
self.y_tilt = y_tilt
def __repr__(self) -> str:
return f"x_tilt: {np.rad2deg(self.x_tilt) :.2f}, y_tilt: {np.rad2deg(self.y_tilt) :.2f}"
class RotationGenerator:
"""This class is used to generate the rotation matrix T(alpha,beta; alpha_0, beta_0"""
def __init__(
self,
new_gonio_pos: GonioPosition,
old_gonio_pos: GonioPosition = GonioPosition(0, 0),
) -> None:
self.new_gonio_pos = new_gonio_pos
self.old_gonio_pos = old_gonio_pos
self.alpha_0 = self.old_gonio_pos.x_tilt
self.beta_0 = self.old_gonio_pos.y_tilt
self.alpha = self.new_gonio_pos.x_tilt - self.old_gonio_pos.x_tilt
self.beta = self.new_gonio_pos.y_tilt - self.old_gonio_pos.y_tilt
def get_T1(self):
a11 = 1
a12 = 0
a13 = 0
a21 = 0
a22 = np.cos(self.alpha)
a23 = -np.sin(self.alpha)
a31 = 0
a32 = np.sin(self.alpha)
a33 = np.cos(self.alpha)
return np.array([[a11, a12, a13],
[a21, a22, a23],
[a31, a32, a33]])
def get_T2(self):
a11 = np.cos(self.beta)
a12 = -np.sin(self.beta) * np.sin(self.alpha_0)
a13 = np.sin(self.beta) * np.cos(self.alpha_0)
a21 = np.sin(self.beta) * np.sin(self.alpha_0)
a22 = np.cos(self.alpha_0) ** 2 + np.sin(self.alpha_0) ** 2 * np.cos(self.beta)
a23 = np.sin(self.alpha_0) * np.cos(self.alpha_0) * (1 - np.cos(self.beta))
a31 = -np.sin(self.beta) * np.cos(self.alpha_0)
a32 = np.sin(self.alpha_0) * np.cos(self.alpha_0) * (1 - np.cos(self.beta))
a33 = np.sin(self.alpha_0) ** 2 + np.cos(self.alpha_0) ** 2 * np.cos(self.beta)
return np.array([[a11, a12, a13],
[a21, a22, a23],
[a31, a32, a33]])
def get_full_rotation_matrix(self):
return self.get_T1() @ self.get_T2()
def rotate_inplane(ori, angle ):
"""This function is used to get the rotation R(theta) converting
between the sample and gonio reference frames."""
rotatior = Rotation.from_axes_angles([0,0,-1], np.deg2rad(angle))
return Orientation(rotatior * ori, symmetry=ori.symmetry)
def transform_to_gonio(ori, new_gonio, old_gonio, tilt_axes_align_angle = 9.61):
"""This function describes the full transformation of a crystal orientation, as described above
The tilt_axes_align_angle parameter is the theta parameter in R(theta)."""
rotgen = RotationGenerator(new_gonio, old_gonio)
rotator = Rotation.from_matrix(rotgen.get_full_rotation_matrix())
ori = ~ori
ori = rotate_inplane(ori, tilt_axes_align_angle)
ori = Orientation(rotator * ori, symmetry = ori.symmetry)
ori = rotate_inplane(ori, -tilt_axes_align_angle)
ori = ~ori
return ori
And applying transform_to_gonio()
to find a desired zone axis by Scipy Minimization
def solve_Gonio_position(current_orientation : Orientation,
current_goniopos : GonioPosition,
desired_zoneaxis : Miller):
equivalent_zoneaxes = desired_zoneaxis.symmetrise()
def smallest_angle_for_tilt(gonio : np.ndarray, current_orientation : Orientation):
new_gonio = GonioPosition(*gonio)
new_orientation = transform_to_gonio(current_orientation, new_gonio, current_goniopos)
angles = np.rad2deg((~new_orientation * equivalent_zoneaxes).angle_with(Miller(uvw=[0,0,1], phase = desired_zoneaxis.phase)))
return np.min(angles)
#Include bounds for the minimization to only give results within the max tilt-range of the sample holder
res = minimize(smallest_angle_for_tilt, np.array([0,0]), args=current_orientation, bounds=((-30,30), (-30,30)), method = "Nelder-Mead")
x_opt =res.x
return GonioPosition(*x_opt)
The application of the transformation described above to find the x- and y-tilt of specific zone axes is very useful on its own (especially for polycrystalline sample with small grains), and has saved me many hours on the TEM. I do think, however, that the transformation could be used for many other problems. For instance, if one is studying slanted grain boundaries in TEM, and somehow manage to describe the normal vector of the grain boundary,
I would be happy to assist with implementing some tutorial for the docs if that is desired, but don't really know where to start with that (code would definitively need a redo, above is only slapped together without any real concern for readability). Also please let me know if you want me to provide any data to show the application of the functions above, or in general have any questions or remarks.