6-circle diffractometer example

The 6-circle diffractometer can be considered as a 4-circle diffractometer with two additional rotations that rotate the sample and detector separately.

Huber 6-circle at APS

Huber 6-circle at APS


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

Since the hklpy package is a thin interface to the hkl library (compiled C++ code), we need to first load the gobject-introspection package (named ``gi``) and name our required code and version.

This is needed every time before the hkl package is first imported.

import gi
gi.require_version('Hkl', '5.0')

Setup the E6C diffractometer in hklpy

In hkl E6C geometry (https://people.debian.org/~picca/hkl/hkl.html#orge5e0490):

E6C geometry

E6C geometry

  • xrays incident on the \(\vec{x}\) direction (1, 0, 0)

axis

moves

rotation axis

vector

mu

sample

\(\vec{z}\)

[0 0 1]

omega

sample

\(-\vec{y}\)

[0 -1 0]

chi

sample

\(\vec{x}\)

[1 0 0]

phi

sample

\(-\vec{y}\)

[0 -1 0]

gamma

detector

\(\vec{z}\)

[0 0 1]

delta

detector

\(-\vec{y}\)

[0 -1 0]

Define this diffractometer

Create a python class that specifies the names of the real-space positioners. We call it SixCircle here but that choice is arbitrary. Pick any valid Python name not already in use.

The argument to the SixCircle class tells which hklpy base class will be used. This sets the geometry. See the hklpy diffractometers documentation for a list of other choices.

In hklpy, the reciprocal-space axes are known as pseudo positioners while the real-space axes are known as real positioners. For the real positioners, it is possible to use different names than the canonical names used internally by the hkl library. That is not covered here.

note: The keyword argument kind="hinted" is an indication that this signal may be plotted.

This demo uses simulated motors. To use EPICS motors, import that structure from ophyd:

from ophyd import EpicsMotor

Then, in the class, replace the real positioners with (substituting with the correct EPICS PV for each motor):

mu = Cpt(EpicsMotor, "pv_prefix:m42", kind="hinted")
omega = Cpt(EpicsMotor, "pv_prefix:m41", kind="hinted")
chi = Cpt(EpicsMotor, "pv_prefix:m22", kind="hinted")
phi = Cpt(EpicsMotor, "pv_prefix:m35", kind="hinted")
gamma = Cpt(EpicsMotor, "pv_prefix:m7", kind="hinted")
delta = Cpt(EpicsMotor, "pv_prefix:m8", kind="hinted")

and, most important, remove the def __init__() method. It is only needed to define an initial position for the simulators. Otherwise, this will move these EPICS motors to zero.

from hkl.diffract import E6C
from ophyd import PseudoSingle, SoftPositioner
from ophyd import Component as Cpt

class SixCircle(E6C):
    """
    Our 6-circle.  Eulerian.
    """
    # 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
    mu = Cpt(SoftPositioner, kind="hinted")
    omega = Cpt(SoftPositioner, kind="hinted")
    chi = Cpt(SoftPositioner, kind="hinted")
    phi = Cpt(SoftPositioner, kind="hinted")
    gamma = Cpt(SoftPositioner, kind="hinted")
    delta = 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
sixc = SixCircle("", name="sixc")

Add a sample with a crystal structure

from hkl.util import Lattice

# add the sample to the calculation engine
a0 = 5.431
sixc.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

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

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

r1 = sixc.calc.sample.add_reflection(
    4, 0, 0,
    position=sixc.calc.Position(
        delta=69.0966,
        omega=-145.451,
        chi=0,
        phi=0,
        mu=0,
        gamma=0,
    )
)

Find the second reflection

r2 = sixc.calc.sample.add_reflection(
    0, 4, 0,
    position=sixc.calc.Position(
        delta=69.0966,
        omega=-145.451,
        chi=90,
        phi=0,
        mu=0,
        gamma=0,
    )
)

Compute the UB orientation matrix

The compute_UB() method always returns 1. Ignore it.

sixc.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", sixc.calc.energy))
tbl.addRow(("wavelength, angstrom", sixc.calc.wavelength))
tbl.addRow(("position", sixc.position))
tbl.addRow(("sample name", sixc.sample_name.get()))
tbl.addRow(("[U]", sixc.U.get()))
tbl.addRow(("[UB]", sixc.UB.get()))
tbl.addRow(("lattice", sixc.lattice.get()))
print(tbl)

print(f"sample\t{sixc.calc.sample}")
==================== ===================================================
term                 value
==================== ===================================================
energy, keV          8.050922077922078
wavelength, angstrom 1.54
position             SixCirclePseudoPos(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 delta in the positive range

  • keep omega in the negative range

  • keep gamma, mu, & phi fixed at zero

sixc.calc["delta"].limits = (-0.001, 180)
sixc.calc["omega"].limits = (-180, 0.001)

for nm in "gamma mu phi".split():
    getattr(sixc, nm).move(0)
    sixc.calc[nm].fit = False
    sixc.calc[nm].value = 0
    sixc.calc[nm].limits = (0, 0)
sixc.engine.mode = "constant_phi_vertical"

Check the inverse calculation: (400)

sol = sixc.inverse((0, -145.451, 0, 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 the inverse calculation: (040)

sol = sixc.inverse((0, -145.451, 90, 0, 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 = sixc.forward((4, 0, 0))
print(
    "(400) :",
    f"tth={sol.delta:.4f}",
    f"omega={sol.omega:.4f}",
    f"chi={sol.chi:.4f}",
    f"phi={sol.phi:.4f}",
    f"mu={sol.mu:.4f}",
    f"gamma={sol.gamma:.4f}",
    )
(400) : tth=69.0985 omega=-145.4500 chi=0.0000 phi=0.0000 mu=0.0000 gamma=0.0000

Check the forward calculation: (040)

sol = sixc.forward((0, 4, 0))
print(
    "(040) :",
    f"tth={sol.delta:.4f}",
    f"omega={sol.omega:.4f}",
    f"chi={sol.chi:.4f}",
    f"phi={sol.phi:.4f}",
    f"mu={sol.mu:.4f}",
    f"gamma={sol.gamma:.4f}",
    )
(040) : tth=69.0985 omega=-145.4500 chi=90.0000 phi=0.0000 mu=0.0000 gamma=0.0000

Check the forward calculation: (440)

sol = sixc.forward((4, 4, 0))
print(
    "(440) :",
    f"tth={sol.delta:.4f}",
    f"omega={sol.omega:.4f}",
    f"chi={sol.chi:.4f}",
    f"phi={sol.phi:.4f}",
    f"mu={sol.mu:.4f}",
    f"gamma={sol.gamma:.4f}",
    )
(440) : tth=106.6471 omega=-126.6755 chi=45.0000 phi=0.0000 mu=0.0000 gamma=0.0000

Scan in reciprocal space using Bluesky

To scan with Bluesky, we need more setup.

%matplotlib inline

from bluesky import RunEngine
from bluesky import SupplementalData
from bluesky.callbacks.best_effort import BestEffortCallback
import bluesky.plans as bp
import bluesky.plan_stubs as bps
import databroker
import matplotlib.pyplot as plt

plt.ion()

bec = BestEffortCallback()
db = databroker.temp().v1
sd = SupplementalData()

RE = RunEngine({})
RE.md = {}
RE.preprocessors.append(sd)
RE.subscribe(db.insert)
RE.subscribe(bec)
1

(h00) scan near (400)

RE(bp.scan([], sixc.h, 3.9, 4.1, 5))
Transient Scan ID: 1     Time: 2020-12-09 00:07:58
Persistent Unique Scan ID: 'f071deae-ca35-41aa-9c25-7bca0233748b'
New stream: 'primary'
+-----------+------------+------------+
|   seq_num |       time |     sixc_h |
+-----------+------------+------------+
|         1 | 00:07:58.7 |      3.900 |
|         2 | 00:07:58.7 |      3.950 |
|         3 | 00:07:58.7 |      4.000 |
|         4 | 00:07:58.8 |      4.050 |
|         5 | 00:07:58.8 |      4.100 |
+-----------+------------+------------+
generator scan ['f071deae'] (scan num: 1)
('f071deae-ca35-41aa-9c25-7bca0233748b',)

chi scan from (400) to (040)

RE(bp.scan([sixc.chi, sixc.h, sixc.k, sixc.l], sixc.chi, 0, 90, 10))
Transient Scan ID: 2     Time: 2020-12-09 00:07:59
Persistent Unique Scan ID: '4f396a5a-358a-4e43-8f9c-5ce95f1afc67'
New stream: 'primary'
+-----------+------------+------------+------------+------------+------------+
|   seq_num |       time |   sixc_chi |     sixc_h |     sixc_k |     sixc_l |
+-----------+------------+------------+------------+------------+------------+
|         1 | 00:07:59.2 |      0.000 |      4.100 |      0.000 |      0.000 |
|         2 | 00:07:59.5 |     10.000 |      4.038 |      0.712 |     -0.000 |
|         3 | 00:07:59.7 |     20.000 |      3.853 |      1.402 |     -0.000 |
|         4 | 00:07:59.9 |     30.000 |      3.551 |      2.050 |     -0.000 |
|         5 | 00:08:00.2 |     40.000 |      3.141 |      2.635 |     -0.000 |
|         6 | 00:08:00.4 |     50.000 |      2.635 |      3.141 |     -0.000 |
|         7 | 00:08:00.6 |     60.000 |      2.050 |      3.551 |     -0.000 |
|         8 | 00:08:00.9 |     70.000 |      1.402 |      3.853 |     -0.000 |
|         9 | 00:08:01.1 |     80.000 |      0.712 |      4.038 |     -0.000 |
|        10 | 00:08:01.3 |     90.000 |      0.000 |      4.100 |      0.000 |
+-----------+------------+------------+------------+------------+------------+
generator scan ['4f396a5a'] (scan num: 2)
('4f396a5a-358a-4e43-8f9c-5ce95f1afc67',)
../../_images/e6c_36_2.svg

(0k0) scan near (040)

RE(bp.scan([], sixc.k, 3.9, 4.1, 5))
Transient Scan ID: 3     Time: 2020-12-09 00:08:02
Persistent Unique Scan ID: 'a1ee3d0f-4860-4b43-a30b-c7a4fa4c8f4d'
New stream: 'primary'
+-----------+------------+------------+
|   seq_num |       time |     sixc_k |
+-----------+------------+------------+
|         1 | 00:08:02.5 |      3.900 |
|         2 | 00:08:02.5 |      3.950 |
|         3 | 00:08:02.5 |      4.000 |
|         4 | 00:08:02.5 |      4.050 |
|         5 | 00:08:02.5 |      4.100 |
+-----------+------------+------------+
generator scan ['a1ee3d0f'] (scan num: 3)
('a1ee3d0f-4860-4b43-a30b-c7a4fa4c8f4d',)

(hk0) scan near (440)

RE(bp.scan([], sixc.h, 3.9, 4.1, sixc.k, 3.9, 4.1, 5))
Transient Scan ID: 4     Time: 2020-12-09 00:08:02
Persistent Unique Scan ID: 'e8f4b12d-1d3c-4481-af0b-2d638cd8f493'
New stream: 'primary'
+-----------+------------+------------+------------+------------+------------+------------+------------+------------+------------+------------+
|   seq_num |       time |     sixc_h |     sixc_k |     sixc_l |    sixc_mu | sixc_omega |   sixc_chi |   sixc_phi | sixc_gamma | sixc_delta |
+-----------+------------+------------+------------+------------+------------+------------+------------+------------+------------+------------+
|         1 | 00:08:03.1 |      3.900 |      3.900 |      0.000 |      0.000 |   -128.558 |     45.000 |      0.000 |      0.000 |    102.883 |
|         2 | 00:08:04.3 |      3.950 |      3.950 |     -0.000 |      0.000 |   -127.627 |     45.000 |      0.000 |      0.000 |    104.745 |
|         3 | 00:08:05.5 |      4.000 |      4.000 |     -0.000 |      0.000 |   -126.675 |     45.000 |      0.000 |      0.000 |    106.647 |
|         4 | 00:08:06.7 |      4.050 |      4.050 |     -0.000 |      0.000 |   -125.703 |     45.000 |      0.000 |      0.000 |    108.593 |
|         5 | 00:08:08.0 |      4.100 |      4.100 |      0.000 |      0.000 |   -124.706 |     45.000 |      0.000 |      0.000 |    110.585 |
+-----------+------------+------------+------------+------------+------------+------------+------------+------------+------------+------------+
generator scan ['e8f4b12d'] (scan num: 4)
('e8f4b12d-1d3c-4481-af0b-2d638cd8f493',)
../../_images/e6c_40_2.svg