4-circle diffractometer example : renamed axes ============================================== Following the E4CV example, this example will repeat those same steps but using different names for the motor axes. ========= ========== E4CV name local name ========= ========== omega theta chi chi phi phi tth ttheta ========= ========== How’s it done? -------------- This change is made by: 1. Defining the ``FourCircle`` class using our local names (instead of the canonical E4CV names) 2. Writing a dictionary to the ``fourc`` object that maps the canonical E4CV names to our local names: .. code:: python fourc.calc.physical_axis_names = { # E4CV: local 'omega': 'theta', 'chi': 'chi', 'phi': 'phi', 'tth': 'ttheta', } -------------- Note: This example is available as a `Jupyter notebook `__ from the *hklpy* source code website: https://github.com/bluesky/hklpy/tree/main/examples Load the *hklpy* package (named *``hkl``*) ------------------------------------------ As done in the E4CV example. This is needed *every* time before the *hkl* package is first imported. .. code:: ipython3 import gi gi.require_version('Hkl', '5.0') Define *this* diffractometer ---------------------------- Following the E4CV example, create a ``FourCircle`` class, but use our own names for the motors. ========= ========== E4CV name local name ========= ========== omega theta chi chi phi phi tth ttheta ========= ========== Use the new motor names when constructing the ``FourCircle`` class. Otherwise, everything else in this step the same as in the E4CV example. .. code:: ipython3 from hkl.diffract import E4CV from ophyd import PseudoSingle, SoftPositioner from ophyd import Component as Cpt class FourCircle(E4CV): """ Our 4-circle. Eulerian, vertical scattering orientation. """ # the reciprocal axes are called: pseudo in hklpy h = Cpt(PseudoSingle, '', kind="hinted") k = Cpt(PseudoSingle, '', kind="hinted") l = Cpt(PseudoSingle, '', kind="hinted") # the motor axes are called: real in hklpy theta = Cpt(SoftPositioner, kind="hinted") chi = Cpt(SoftPositioner, kind="hinted") phi = Cpt(SoftPositioner, kind="hinted") ttheta = Cpt(SoftPositioner, kind="hinted") def __init__(self, *args, **kwargs): """Define an initial position for simulators.""" super().__init__(*args, **kwargs) for p in self.real_positioners: p._set_position(0) # give each a starting position Create an instance of the diffractometer. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As in the E4CV example, create an instance of the diffractometer. Note that the instance still has the canonical E4CV names, as reported by libhkl. .. code:: ipython3 fourc = FourCircle("", name="fourc") print("motor names:", fourc.calc.physical_axis_names) .. parsed-literal:: motor names: ['omega', 'chi', 'phi', 'tth'] Switch to **our** motor names ----------------------------- This is the magic step, *map* the canonical libhkl names onto the names we want to use. This is done using a Python dictionary. They keys are the canonical names, the value of key is the local name. *All* axes must be in the dictionary, even if the names remain the same. .. code:: ipython3 fourc.calc.physical_axis_names = { # E4CV: local 'omega': 'theta', 'chi': 'chi', 'phi': 'phi', 'tth': 'ttheta', } print("motor names:", fourc.calc.physical_axis_names) .. parsed-literal:: mapping: {'omega': 'theta', 'chi': 'chi', 'phi': 'phi', 'tth': 'ttheta'} motor names: ['theta', 'chi', 'phi', 'ttheta'] Add a sample with a crystal structure ------------------------------------- .. code:: ipython3 from hkl.util import Lattice # add the sample to the calculation engine a0 = 5.431 fourc.calc.new_sample( "silicon", lattice=Lattice(a=a0, b=a0, c=a0, alpha=90, beta=90, gamma=90) ) .. parsed-literal:: HklSample(name='silicon', lattice=LatticeTuple(a=5.431, b=5.431, c=5.431, alpha=90.0, beta=90.0, gamma=90.0), ux=Parameter(name='None (internally: ux)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree'), uy=Parameter(name='None (internally: uy)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree'), uz=Parameter(name='None (internally: uz)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree'), U=array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]), UB=array([[ 1.15691131e+00, -7.08403864e-17, -7.08403864e-17], [ 0.00000000e+00, 1.15691131e+00, -7.08403864e-17], [ 0.00000000e+00, 0.00000000e+00, 1.15691131e+00]]), reflections=[]) Setup the UB orientation matrix using *hklpy* --------------------------------------------- Define the crystal’s orientation on the diffractometer using the 2-reflection method described by `Busing & Levy, Acta Cryst 22 (1967) 457 `__. Choose the same wavelength X-rays for both reflections ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 fourc.calc.wavelength = 1.54 # Angstrom (8.0509 keV) Find the first reflection and identify its Miller indices: (*hkl*) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 r1 = fourc.calc.sample.add_reflection( 4, 0, 0, position=fourc.calc.Position( ttheta=69.0966, theta=-145.451, chi=0, phi=0, ) ) Find the second reflection ~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 r2 = fourc.calc.sample.add_reflection( 0, 4, 0, position=fourc.calc.Position( ttheta=69.0966, theta=-145.451, chi=90, phi=0, ) ) Compute the *UB* orientation matrix ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``compute_UB()`` method always returns 1. Ignore it. .. code:: ipython3 fourc.calc.sample.compute_UB(r1, r2) .. parsed-literal:: 1 Report what we have setup ------------------------- .. code:: ipython3 import pyRestTable tbl = pyRestTable.Table() tbl.labels = "term value".split() tbl.addRow(("energy, keV", fourc.calc.energy)) tbl.addRow(("wavelength, angstrom", fourc.calc.wavelength)) tbl.addRow(("position", fourc.position)) tbl.addRow(("sample name", fourc.sample_name.get())) tbl.addRow(("[U]", fourc.U.get())) tbl.addRow(("[UB]", fourc.UB.get())) tbl.addRow(("lattice", fourc.lattice.get())) print(tbl) print(f"sample\t{fourc.calc.sample}") .. parsed-literal:: ==================== =================================================== term value ==================== =================================================== energy, keV 8.050922077922078 wavelength, angstrom 1.54 position FourCirclePseudoPos(h=-0.0, k=0.0, l=0.0) sample name silicon [U] [[-1.22173048e-05 -1.22173048e-05 -1.00000000e+00] [ 0.00000000e+00 -1.00000000e+00 1.22173048e-05] [-1.00000000e+00 1.49262536e-10 1.22173048e-05]] [UB] [[-1.41343380e-05 -1.41343380e-05 -1.15691131e+00] [ 0.00000000e+00 -1.15691131e+00 1.41343380e-05] [-1.15691131e+00 1.72683586e-10 1.41343380e-05]] lattice [ 5.431 5.431 5.431 90. 90. 90. ] ==================== =================================================== sample HklSample(name='silicon', lattice=LatticeTuple(a=5.431, b=5.431, c=5.431, alpha=90.0, beta=90.0, gamma=90.0), ux=Parameter(name='None (internally: ux)', limits=(min=-180.0, max=180.0), value=-45.0, fit=True, inverted=False, units='Degree'), uy=Parameter(name='None (internally: uy)', limits=(min=-180.0, max=180.0), value=-89.99901005102187, fit=True, inverted=False, units='Degree'), uz=Parameter(name='None (internally: uz)', limits=(min=-180.0, max=180.0), value=135.00000000427607, fit=True, inverted=False, units='Degree'), U=array([[-1.22173048e-05, -1.22173048e-05, -1.00000000e+00], [ 0.00000000e+00, -1.00000000e+00, 1.22173048e-05], [-1.00000000e+00, 1.49262536e-10, 1.22173048e-05]]), UB=array([[-1.41343380e-05, -1.41343380e-05, -1.15691131e+00], [ 0.00000000e+00, -1.15691131e+00, 1.41343380e-05], [-1.15691131e+00, 1.72683586e-10, 1.41343380e-05]]), reflections=[(h=4.0, k=0.0, l=0.0), (h=0.0, k=4.0, l=0.0)], reflection_measured_angles=array([[0. , 1.57079633], [1.57079633, 0. ]]), reflection_theoretical_angles=array([[0. , 1.57079633], [1.57079633, 0. ]])) Check the orientation matrix ---------------------------- Perform checks with *forward* (hkl to angle) and *inverse* (angle to hkl) computations to verify the diffractometer will move to the same positions where the reflections were identified. Constrain the motors to limited ranges ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - allow for slight roundoff errors - keep ``ttheta`` in the positive range - keep ``theta`` in the negative range - keep ``phi`` fixed at zero .. code:: ipython3 fourc.calc["ttheta"].limits = (-0.001, 180) fourc.calc["theta"].limits = (-180, 0.001) fourc.phi.move(0) fourc.engine.mode = "constant_phi" Check the inverse calculation: (400) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 sol = fourc.inverse((-145.451, 0, 0, 69.0966)) print("(4 0 0) ?", f"{sol.h:.2f}", f"{sol.k:.2f}", f"{sol.l:.2f}") .. parsed-literal:: (4 0 0) ? 4.00 0.00 0.00 Check another inverse calculation: (040) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 sol = fourc.inverse((-145.451, 90, 0, 69.0966)) print("(0 4 0) ?", f"{sol.h:.2f}", f"{sol.k:.2f}", f"{sol.l:.2f}") .. parsed-literal:: (0 4 0) ? 0.00 4.00 0.00 Check the forward calculation: (400) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: ipython3 sol = fourc.forward((4, 0, 0)) print( "(400) :", f"ttheta={sol.ttheta:.4f}", f"theta={sol.theta:.4f}", f"chi={sol.chi:.4f}", f"phi={sol.phi:.4f}" ) .. parsed-literal:: (400) : ttheta=69.0985 theta=-145.4500 chi=0.0000 phi=0.0000 Continue the E4CV example on your own…