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:

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.

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.

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.

fourc = FourCircle("", name="fourc")
print("motor names:", fourc.calc.physical_axis_names)
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.

fourc.calc.physical_axis_names = {
    # E4CV: local
    'omega': 'theta',
    'chi': 'chi',
    'phi': 'phi',
    'tth': 'ttheta',
    }

print("motor names:", fourc.calc.physical_axis_names)
mapping: {'omega': 'theta', 'chi': 'chi', 'phi': 'phi', 'tth': 'ttheta'}
motor names: ['theta', 'chi', 'phi', 'ttheta']

Add a sample with a crystal structure

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

fourc.calc.wavelength = 1.54 # Angstrom (8.0509 keV)

Find the first reflection and identify its Miller indices: (hkl)

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

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.

fourc.calc.sample.compute_UB(r1, r2)
1

Report what we have setup

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}")
==================== ===================================================
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

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)

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}")
(4 0 0) ? 4.00 0.00 0.00

Check another inverse calculation: (040)

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}")
(0 4 0) ? 0.00 4.00 0.00

Check the forward calculation: (400)

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}"
    )
(400) : ttheta=69.0985 theta=-145.4500 chi=0.0000 phi=0.0000

Continue the E4CV example on your own…