Lesson 7: 4-circle diffractometer#

Operate a four-circle diffractometer using the hkl package. The lesson will first show how to setup the diffractometer and demonstrate the basic features of the software. Then, the lesson will setup a diffractometer with a single crystal sample, orient it, and scan along one axis of reciprocal space. Examples are then provided for several different crystals.

The hkl package is a python interface to the C++ hkl library, written by Frederic Picca.

Documentation for the C++ library is here: https://people.debian.org/~picca/hkl/hkl.html

Setup#

Setup involves starting the bluesky session, importing various support packages, and defining any custom classes or functions as needed.

Preparation#

Import the instrument package as our routine initialization.

[1]:
from instrument.collection import *
I Wed-17:24:44 - ############################################################ startup
I Wed-17:24:44 - logging started
I Wed-17:24:44 - logging level = 10
I Wed-17:24:44 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/collection.py
I Wed-17:24:44 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/mpl/notebook.py
Activating auto-logging. Current session state plus future input saved.
Filename       : /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/.logs/ipython_console.log
Mode           : rotate
Output logging : True
Raw input log  : False
Timestamping   : True
State          : active
I Wed-17:24:45 - bluesky framework
I Wed-17:24:45 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/framework/check_python.py
I Wed-17:24:45 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/framework/check_bluesky.py
I Wed-17:24:46 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/framework/initialize.py
I Wed-17:24:47 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/framework/metadata.py
I Wed-17:24:47 - /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/instrument/framework/callbacks.py
I Wed-17:24:47 - writing to SPEC file: /home/prjemian/Documents/projects/BCDA-APS/bluesky_training/lessons/20201216-172447.dat
I Wed-17:24:47 -    >>>>   Using default SPEC file name   <<<<
I Wed-17:24:47 -    file will be created when bluesky ends its next scan
I Wed-17:24:47 -    to change SPEC file, use command:   newSpecFile('title')

After starting a bluesky console or notebook session and after importing the instrument package, import the python gobject-introspection package that is required to load the hkl support library. This step must happen before the hkl package is first imported.

[2]:
import gi
gi.require_version('Hkl', '5.0')

Next, import the desired diffractometer geometry from the hklpy package. We pick E4CV (Eulerian 4-Circle with Vertical scattering geometry) as is typical at synchrotron beamlines.

[3]:
from hkl.diffract import E4CV
from hkl.util import Lattice

Next, we get additional packages that we may use.

[4]:
from apstools.diffractometer import Constraint
from apstools.diffractometer import DiffractometerMixin

from bluesky import plans as bp
from bluesky import plan_stubs as bps

from ophyd import Component
from ophyd import PseudoSingle
from ophyd import SoftPositioner

The diffractometer object#

Define a 4-circle class for our example with simulated motors. There are attributes for the reciprocal space axes h, k, & l and for the real space axes: phi, omega, chi, & tth.

[5]:
class FourCircleDiffractometer(DiffractometerMixin, E4CV):
    h = Component(PseudoSingle, '',
        labels=("hkl", "fourc"), kind="hinted")
    k = Component(PseudoSingle, '',
        labels=("hkl", "fourc"), kind="hinted")
    l = Component(PseudoSingle, '',
        labels=("hkl", "fourc"), kind="hinted")

    omega = Component(SoftPositioner,
        labels=("motor", "fourc"), kind="hinted")
    chi =   Component(SoftPositioner,
        labels=("motor", "fourc"), kind="hinted")
    phi =   Component(SoftPositioner,
        labels=("motor", "fourc"), kind="hinted")
    tth =   Component(SoftPositioner,
        labels=("motor", "fourc"), kind="hinted")

    def __init__(self, *args, **kwargs):
        """
        start the SoftPositioner objects with initial values

        Since this diffractometer uses simulated motors,
        prime the SoftPositioners (motors) with initial values.
        Otherwise, with position == None, then describe(), and
        other functions get borked.
        """
        super().__init__(*args, **kwargs)

        for axis in self.real_positioners:
            axis.move(0)

use EPICS motors instead of simulators

To use EPICS motors (ophyd.EpicsMotor) instead of the ophyd.SoftPositioner simulators, redefine the FourCircleDiffractometer class (or define a new class) as follows, substituting with the proper motor PVs.

NOTE: Unlike the example above with SoftPositioner objects as motors, do not need to initialize the motor positions in here since EPICS has already initialized each of the motors.

class FourCircleDiffractometer(DiffractometerMixin, E4CV):
    h = Component(PseudoSingle, '',
        labels=("hkl", "fourc"), kind="hinted")
    k = Component(PseudoSingle, '',
        labels=("hkl", "fourc"), kind="hinted")
    l = Component(PseudoSingle, '',
        labels=("hkl", "fourc"), kind="hinted")

    omega = Component(EpicsMotor, "ioc:m1",
        labels=("motor", "fourc"), kind="hinted")
    chi =   Component(EpicsMotor, "ioc:m2",
        labels=("motor", "fourc"), kind="hinted")
    phi =   Component(EpicsMotor, "ioc:m3",
        labels=("motor", "fourc"), kind="hinted")
    tth =   Component(EpicsMotor, "ioc:m4",
        labels=("motor", "fourc"), kind="hinted")

Create the diffractometer object:

[6]:
fourc = FourCircleDiffractometer('', name='fourc')

The fourc.wh() method provides a quick summary of the diffractometer:

[7]:
fourc.wh()
===================== ========= =========
term                  value     axis_type
===================== ========= =========
diffractometer        fourc
sample name           main
energy (keV)          8.05092
wavelength (angstrom) 1.54000
calc engine           hkl
mode                  bissector
h                     0.0       pseudo
k                     0.0       pseudo
l                     0.0       pseudo
omega                 0         real
chi                   0         real
phi                   0         real
tth                   0         real
===================== ========= =========

[7]:
<pyRestTable.rest_table.Table at 0x7f7e640434f0>

Print the value of the omega axis:

[8]:
print(fourc.omega)
SoftPositioner(name='fourc_omega', parent='fourc', settle_time=0.0, timeout=None, egu='', limits=(0, 0), source='computed')

That’s the object. It’s .position property shows the position.

[9]:
print(fourc.omega.position)
0

Use the %mov magic command to move a motor:

[10]:
%mov fourc.omega 1
print(fourc.omega.position)
%mov fourc.omega 0
print(fourc.omega.position)
1
0

The diffractometer reciprocal-space coordinates are available:

[11]:
print(fourc.position)
FourCircleDiffractometerPseudoPos(h=0.0, k=0.0, l=0.0)

When a diffractometer object is first created, it comes pre-defined with certain defaults:

  • operating mode

  • wavelength

  • sample

  • orientation matrix

  • axis constraints

The sections below will cover each of these.

Operating mode#

The default operating mode is bissector. This mode constrains tth to equal 2*omega.

[12]:
fourc.calc.engine.mode
[12]:
'bissector'

Print the list of available operating modes:

[13]:
print(fourc.engine.modes)
['bissector', 'constant_omega', 'constant_chi', 'constant_phi', 'double_diffraction', 'psi_constant']

Change to constant_phi mode:

[14]:
fourc.calc.engine.mode = "constant_phi"
print(fourc.calc.engine.mode)
constant_phi

Change it back:

[15]:
fourc.calc.engine.mode = "bissector"
print(fourc.calc.engine.mode)
bissector

Wavelength#

The default wavelength is 1.54 angstroms. The units must match the units of the unit cell.

[16]:
fourc.calc.wavelength
[16]:
1.54

Change the wavelength (use angstrom):

[17]:
fourc.calc.wavelength = 1.62751693358

NOTE: Stick to wavelength, at least for now. Do not specify X-ray photon energy. For the hkl code, specify wavelength in angstrom. While the documentation may state wavelength in nm, use angstrom since these units must match the units used for the lattice parameters. Also, note that the (internal) calculation of X-ray energy assumes the units were nm so its conversion between energy and wavelength is off by a factor of 10.

Sample#

The default sample is named main and is a hypothetical cubic lattice with 1.54 angstrom edges.

[18]:
fourc.calc.sample
[18]:
HklSample(name='main', lattice=LatticeTuple(a=1.54, b=1.54, c=1.54, 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([[ 4.07999046e+00, -2.49827363e-16, -2.49827363e-16],
       [ 0.00000000e+00,  4.07999046e+00, -2.49827363e-16],
       [ 0.00000000e+00,  0.00000000e+00,  4.07999046e+00]]), reflections=[])

The diffractometer support maintains a dictionary of all defined samples (fourc.calc._samples). The fourc.calc.sample symbol points to one of these. Let’s illustrate by creating a new sample named orthorhombic:

[19]:
fourc.calc.new_sample('orthorhombic',
    lattice=Lattice(
        a=1, b=2, c=3,
        alpha=90.0, beta=90.0, gamma=90.0))
[19]:
HklSample(name='orthorhombic', lattice=LatticeTuple(a=1.0, b=2.0, c=3.0, 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([[ 6.28318531e+00, -1.92367069e-16, -1.28244713e-16],
       [ 0.00000000e+00,  3.14159265e+00, -1.28244713e-16],
       [ 0.00000000e+00,  0.00000000e+00,  2.09439510e+00]]), reflections=[])

Now, there are two samples defined. The fourc.calc.sample symbol points to the new one:

[20]:
len(fourc.calc._samples)
[20]:
2

Show the name of the current sample:

[21]:
print(fourc.calc.sample.name)
orthorhombic

Switch back to the main sample:

[22]:
fourc.calc.sample = "main"
print(fourc.calc.sample.name)
main

Let’s create another sample and define an orientation:

[23]:
fourc.calc.new_sample('EuPtIn4_eh1_ver',
    lattice=Lattice(
        a=4.542, b=16.955, c=7.389,
        alpha=90.0, beta=90.0, gamma=90.0))

[23]:
HklSample(name='EuPtIn4_eh1_ver', lattice=LatticeTuple(a=4.542, b=16.955, c=7.389, 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.38335212e+00, -2.26914856e-17, -5.20684990e-17],
       [ 0.00000000e+00,  3.70580083e-01, -5.20684990e-17],
       [ 0.00000000e+00,  0.00000000e+00,  8.50343119e-01]]), reflections=[])

This is the orientation matrix defined by default:

[24]:
fourc.calc.sample.U
[24]:
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
[25]:
fourc.calc.sample.UB
[25]:
array([[ 1.38335212e+00, -2.26914856e-17, -5.20684990e-17],
       [ 0.00000000e+00,  3.70580083e-01, -5.20684990e-17],
       [ 0.00000000e+00,  0.00000000e+00,  8.50343119e-01]])

Change it by providing a new 3x3 array:

[26]:
fourc.calc.sample.U = [[0, 1, 0], [0,0,1], [1,0,0]]
fourc.calc.sample.U
[26]:
array([[0., 1., 0.],
       [0., 0., 1.],
       [1., 0., 0.]])
[27]:
fourc.calc.sample.UB
[27]:
array([[ 0.00000000e+00,  3.70580083e-01, -5.20684990e-17],
       [ 0.00000000e+00,  0.00000000e+00,  8.50343119e-01],
       [ 1.38335212e+00, -2.26914856e-17, -5.20684990e-17]])

Set it back:

[28]:
fourc.calc.sample.U = [[1,0,0], [0, 1, 0], [0,0,1]]
print("[U]:\n", fourc.calc.sample.U)
print("[UB]:\n", fourc.calc.sample.UB)
[U]:
 [[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
[UB]:
 [[ 1.38335212e+00 -2.26914856e-17 -5.20684990e-17]
 [ 0.00000000e+00  3.70580083e-01 -5.20684990e-17]
 [ 0.00000000e+00  0.00000000e+00  8.50343119e-01]]

The UB matrix (fourc.calc.sample.UB) can be changed in similar fashion.

The diffractometer reciprocal-space coordinates are available:

[29]:
fourc.position
[29]:
FourCircleDiffractometerPseudoPos(h=0.0, k=0.0, l=0.0)

Move the diffractometer to the (020) reflection:

[30]:
fourc.move(0, 2, 0)
fourc.position
[30]:
FourCircleDiffractometerPseudoPos(h=1.5121846940302517e-16, k=1.9999999974773768, l=4.6102208393375324e-10)

Reflections#

A reflection associates a set of reciprocal-space axes (hkl) with a set of real-space motor positions. Following the method of Busing & Levy (Acta Cryst (1967) 22, pp 457-464), two reflections are used to calculate an orientation matix (UB matrix) which is used to convert between motor positions and hkl values.

There are no reflections defined by default.

Define a reflection by associating a known hkl reflection with a set of motor positions.

[31]:
rp1 = fourc.calc.Position(omega=22.31594, chi=89.1377, phi=0, tth=45.15857)
r1 = fourc.calc.sample.add_reflection(0, 8, 0, position=rp1)

Define a second reflection (that is not a multiple of the first reflection):

[32]:
rp2 = fourc.calc.Position(omega=34.96232, chi=78.3139, phi=0, tth=71.8007)
r2 = fourc.calc.sample.add_reflection(0, 12, 1, position=rp2)

Calculate the UB matrix from these two reflections.

[33]:
fourc.calc.sample.compute_UB(r1, r2)
print(fourc.calc.sample.UB)
[[ 1.38058756 -0.00170327 -0.05359029]
 [ 0.00503279  0.3705342  -0.01301801]
 [ 0.08726811  0.00557695  0.8485529 ]]

Forward Solutions#

Forward solutions are the calculated combinations of real-space motor positions given the sample oreitnation matrix, reciprocal-space axes, operating mode, wavelength, and applied constraints. These combinations are presented as a python list which may be empty if there are no solutions.

Show default solution for a few reflections (100), (010) and (001):

[34]:
r = []  # list of reflections - (hkl) tuples
r.append((1,0,0))
r.append((0,1,0))
r.append((0,0,1))
print(fourc.forwardSolutionsTable(r))
========= ======== ========= ========== ========= =========
(hkl)     solution omega     chi        phi       tth
========= ======== ========= ========== ========= =========
(1, 0, 0) 0        -10.32101 -179.79155 86.38310  -20.64202
(0, 1, 0) 0        -2.75098  -90.90161  -16.98318 -5.50196
(0, 0, 1) 0        6.32287   -0.87718   -3.61371  12.64574
========= ======== ========= ========== ========= =========

NOTE: The method to select the default solution can be changed. See decision_fcn in https://blueskyproject.io/hklpy/master/calc.html#hkl.calc.CalcRecip.forward_iter.

Show all the possible solutions by adding the full=True keyword:

[35]:
print(fourc.forwardSolutionsTable(r, full=True))
========= ======== ========== ========== ========= =========
(hkl)     solution omega      chi        phi       tth
========= ======== ========== ========== ========= =========
(1, 0, 0) 0        -10.32101  -179.79155 86.38310  -20.64202
(1, 0, 0) 1        -10.32101  -0.20845   -93.61690 -20.64202
(1, 0, 0) 2        10.32101   0.20845    86.38310  20.64202
(1, 0, 0) 3        -169.67899 -179.79155 86.38310  20.64202
(1, 0, 0) 4        -169.67899 -0.20845   -93.61690 20.64202
(1, 0, 0) 5        10.32101   179.79155  -93.61690 20.64202
(0, 1, 0) 0        -2.75098   -90.90161  -16.98318 -5.50196
(0, 1, 0) 1        -2.75098   -89.09839  163.01682 -5.50196
(0, 1, 0) 2        -177.24902 -90.90161  -16.98318 5.50196
(0, 1, 0) 3        2.75098    89.09839   -16.98318 5.50196
(0, 1, 0) 4        -177.24902 -89.09839  163.01682 5.50196
(0, 1, 0) 5        2.75098    90.90161   163.01682 5.50196
(0, 0, 1) 0        6.32287    -0.87718   -3.61371  12.64574
(0, 0, 1) 1        -6.32287   0.87718    176.38629 -12.64574
(0, 0, 1) 2        -6.32287   179.12282  -3.61371  -12.64574
(0, 0, 1) 3        6.32287    -179.12282 176.38629 12.64574
(0, 0, 1) 4        -173.67713 0.87718    176.38629 12.64574
(0, 0, 1) 5        -173.67713 179.12282  -3.61371  12.64574
========= ======== ========== ========== ========= =========

For each of the reflections, six solutions were found possible. The first solution is taken as the default. To make a different choice, access the complete list, such as:

[36]:
fourc.calc.forward((1,0,0))
[36]:
(PosCalcE4CV(omega=-10.321012305373241, chi=-179.79155096044508, phi=86.38309742089963, tth=-20.642024610746482),
 PosCalcE4CV(omega=-10.321012305373241, chi=-0.20844903955490512, phi=-93.61690257910038, tth=-20.642024610746482),
 PosCalcE4CV(omega=10.321012305373241, chi=0.20844903955490512, phi=86.38309742089963, tth=20.642024610746482),
 PosCalcE4CV(omega=-169.67898769462678, chi=-179.79155096044508, phi=86.38309742089963, tth=20.642024610746482),
 PosCalcE4CV(omega=-169.67898769462678, chi=-0.20844903955490512, phi=-93.61690257910038, tth=20.642024610746482),
 PosCalcE4CV(omega=10.321012305373241, chi=179.79155096044508, phi=-93.61690257910038, tth=20.642024610746482))

Alternatively, apply one or more constraints to restrict the range of allowed solutions.

If there are no solutions to the forward calculation, the hkl package raises a ValueError exception:

ValueError: Calculation failed (hkl-mode-auto-error-quark: none of the functions were solved !!! (0))

detailed exception trace

In [114]: fourc.calc.forward((5, 4, 35))
---------------------------------------------------------------------------
Error                                     Traceback (most recent call last)
~/.conda/envs/bluesky_2020_9/lib/python3.8/site-packages/hkl/engine.py in pseudo_positions(self, values)
    212         try:
--> 213             geometry_list = self._engine.pseudo_axis_values_set(values,
    214                                                                 self._units)

Error: hkl-mode-auto-error-quark: none of the functions were solved !!! (0)

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
<ipython-input-114-7b5743692787> in <module>
----> 1 fourc.calc.forward((5, 4, 35))

~/.conda/envs/bluesky_2020_9/lib/python3.8/site-packages/hkl/calc.py in wrapped(self, *args, **kwargs)
     42             initial_pos = self.physical_positions
     43             try:
---> 44                 return func(self, *args, **kwargs)
     45             finally:
     46                 self.physical_positions = initial_pos

~/.conda/envs/bluesky_2020_9/lib/python3.8/site-packages/hkl/calc.py in forward(self, position, engine)
    505                 raise ValueError('Engine unset')
    506
--> 507             self.engine.pseudo_positions = position
    508             return self.engine.solutions
    509

~/.conda/envs/bluesky_2020_9/lib/python3.8/site-packages/hkl/engine.py in pseudo_positions(self, values)
    214                                                                 self._units)
    215         except GLib.GError as ex:
--> 216             raise ValueError('Calculation failed (%s)' % ex)
    217
    218         Position = self._calc.Position

ValueError: Calculation failed (hkl-mode-auto-error-quark: none of the functions were solved !!! (0))

The forwardSolutionsTable does not raise an error but displays none for any hkl reflection with no solution in the real-space motor positions. To demonstrate, the (5 4 35) reflection is not available at this wavelength:

[37]:
print(fourc.forwardSolutionsTable( [ (5,4,35), ], full=True))
========== ======== ===== === === ===
(hkl)      solution omega chi phi tth
========== ======== ===== === === ===
(5, 4, 35) none
========== ======== ===== === === ===

Constraints#

Constraints are applied to restrict the motor positions that are allowed to be solutions of the forward calculation from a reflection hkl to motor positions.

[38]:
fourc.showConstraints()
===== ========= ========== ================== ====
axis  low_limit high_limit value              fit
===== ========= ========== ================== ====
omega -180.0    180.0      -5.50832505964632  True
chi   -180.0    180.0      -90.00000003030587 True
phi   -180.0    180.0      0.0                True
tth   -180.0    180.0      -11.01665011929264 True
===== ========= ========== ================== ====

[38]:
<pyRestTable.rest_table.Table at 0x7f7e6403f100>

Apply a constraint that only allows non-negative values for omega. Create a dictionary with the axis constraints to be applied. The dictionary key is the axis name (must be a name in the fourc.calc.physical_axis_names list).

The names of the axes are available:

[39]:
fourc.calc.physical_axis_names
[39]:
['omega', 'chi', 'phi', 'tth']

The value is a Constraints object.

Constraints Arguments

  • low_limit (number) : Limit solutions for this axis to no less than low_limit when fit=True.

  • high_limit (number) : Limit solutions for this axis to no greater than high_limit when fit=True.

  • value (number) : Calculate with axis = value when fit=False.

  • fit (bool) : Not used.

In the dictionary, it is only necessary to define the axis constraints to be changed. The other (commented out) constraints will not be changed.

[40]:
my_constraints = {
    # axis: Constraint(lo_limit, hi_limit, value, fit)
    "omega": Constraint(0, 180, 0, True),
    # "chi": Constraint(-180, 180, 0, True),
    # "phi": Constraint(-180, 180, 0, True),
    # "tth": Constraint(-180, 180, 0, True),
}
fourc.applyConstraints(my_constraints)
fourc.showConstraints()
===== ========= ========== ================== ====
axis  low_limit high_limit value              fit
===== ========= ========== ================== ====
omega 0.0       180.0      0.0                True
chi   -180.0    180.0      -90.00000003030587 True
phi   -180.0    180.0      0.0                True
tth   -180.0    180.0      -11.01665011929264 True
===== ========= ========== ================== ====

[40]:
<pyRestTable.rest_table.Table at 0x7f7e57b64a30>

Show all the possible solutions with these constraints (set keyword argument full=True):

[41]:
print(fourc.forwardSolutionsTable([[1,0,0],[0,1,0]], full=True))
========= ======== ======== ========= ========= ========
(hkl)     solution omega    chi       phi       tth
========= ======== ======== ========= ========= ========
[1, 0, 0] 0        10.32101 0.20845   86.38310  20.64202
[1, 0, 0] 1        10.32101 179.79155 -93.61690 20.64202
[0, 1, 0] 0        2.75098  89.09839  -16.98335 5.50196
[0, 1, 0] 1        2.75098  90.90161  163.01665 5.50196
========= ======== ======== ========= ========= ========

Remove the constraint on omega and show the solutions:

[42]:
fourc.undoLastConstraints()
fourc.showConstraints()
print(fourc.forwardSolutionsTable([[1,0,0],[0,1,0]], full=True))
print("\n\n Now, just the default solutions:")
print(fourc.forwardSolutionsTable([[1,0,0],[0,1,0]]))
===== ========= ========== ================== ====
axis  low_limit high_limit value              fit
===== ========= ========== ================== ====
omega -180.0    180.0      -5.50832505964632  True
chi   -180.0    180.0      -90.00000003030587 True
phi   -180.0    180.0      0.0                True
tth   -180.0    180.0      -11.01665011929264 True
===== ========= ========== ================== ====

========= ======== ========== ========== ========= =========
(hkl)     solution omega      chi        phi       tth
========= ======== ========== ========== ========= =========
[1, 0, 0] 0        -10.32101  -179.79155 86.38310  -20.64202
[1, 0, 0] 1        -10.32101  -0.20845   -93.61690 -20.64202
[1, 0, 0] 2        10.32101   0.20845    86.38310  20.64202
[1, 0, 0] 3        -169.67899 -179.79155 86.38310  20.64202
[1, 0, 0] 4        -169.67899 -0.20845   -93.61690 20.64202
[1, 0, 0] 5        10.32101   179.79155  -93.61690 20.64202
[0, 1, 0] 0        -2.75098   -90.90161  -16.98318 -5.50196
[0, 1, 0] 1        -2.75098   -89.09839  163.01682 -5.50196
[0, 1, 0] 2        -177.24902 -90.90161  -16.98318 5.50196
[0, 1, 0] 3        2.75098    89.09839   -16.98318 5.50196
[0, 1, 0] 4        -177.24902 -89.09839  163.01682 5.50196
[0, 1, 0] 5        2.75098    90.90161   163.01682 5.50196
========= ======== ========== ========== ========= =========



 Now, just the default solutions:
========= ======== ========= ========== ========= =========
(hkl)     solution omega     chi        phi       tth
========= ======== ========= ========== ========= =========
[1, 0, 0] 0        -10.32101 -179.79155 86.38310  -20.64202
[0, 1, 0] 0        -2.75098  -90.90161  -16.98318 -5.50196
========= ======== ========= ========== ========= =========


Example - 4-circle with LNO_LAO sample#

We have some example information from a SPEC data file (collected at APS beamline 33BM). In summary, the sample and orientation information extracted is:

term

value(s)

sample

LNO_LAO

crystal

3.781726143 3.791444574 3.79890313 90.2546203 90.01815424 89.89967858

geometry

fourc

mode

0 (Omega equals zero)

lambda

1.239424258

r1

(0, 0, 2) 38.09875 19.1335 90.0135 0

r2

(1, 1, 3) 65.644 32.82125 115.23625 48.1315

Q

(2, 2, 1.9) 67.78225 33.891 145.985 48.22875 -0.001 -0.16

UB[0]

-1.658712442 0.09820024135 -0.000389705578

UB[1]

-0.09554990312 -1.654278629 0.00242844486

UB[2]

0.0002629818914 0.009815746824 1.653961812

Note: In SPEC’s fourc geometry, the motors are reported in this order: tth omega chi phi

use 4-circle geometry#

Reset all the above changes by re-creating the fourc object.

[43]:
fourc = FourCircleDiffractometer('', name='fourc')

define the sample#

[44]:
fourc.calc.new_sample('LNO_LAO',
    lattice=Lattice(
        a=3.781726143, b=3.791444574 , c=3.79890313,
        alpha=90.2546203, beta=90.01815424, gamma=89.89967858))
[44]:
HklSample(name='LNO_LAO', lattice=LatticeTuple(a=3.781726143, b=3.791444574, c=3.79890313, alpha=90.2546203, beta=90.01815424, gamma=89.89967858), 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.66146225e+00, -2.89938471e-03,  5.11196668e-04],
       [ 0.00000000e+00,  1.65721725e+00,  7.34922202e-03],
       [ 0.00000000e+00,  0.00000000e+00,  1.65394723e+00]]), reflections=[])

set wavelength#

[45]:
fourc.calc.wavelength = 1.239424258

define two reflections#

[46]:
rp1 = fourc.calc.Position(omega=19.1335, chi=90.0135, phi=0, tth=38.09875)
r1 = fourc.calc.sample.add_reflection(0, 0, 2, position=rp1)
rp2 = fourc.calc.Position(omega=32.82125, chi=115.23625, phi=48.1315, tth=65.644)
r2 = fourc.calc.sample.add_reflection(1, 1, 3, position=rp2)

calculate UB matrix#

[47]:
fourc.calc.sample.compute_UB(r1, r2)
fourc.calc.sample.UB
[47]:
array([[-9.55499011e-02, -1.65427863e+00,  2.42844485e-03],
       [ 2.62981975e-04,  9.81483906e-03,  1.65396181e+00],
       [-1.65871244e+00,  9.82002396e-02, -3.89705577e-04]])

Compare this result with the UB matrix computed by SPEC:

-1.658712442        0.09820024135       -0.000389705578
-0.09554990312      -1.654278629        0.00242844486
0.0002629818914     0.009815746824      1.653961812

Same numbers, different row order, different order of real-space motors.

calculate (hkl) given motor positions#

Given these motor positions, confirm this is the (2 2 1.9) reflection.

axis

value

omega

33.891

chi

145.985

phi

48.22875

tth

67.78225

[48]:
%mov fourc.omega 33.891 fourc.chi 145.985 fourc.phi 48.22875 fourc.tth 67.78225
fourc.wh()
===================== ================== =========
term                  value              axis_type
===================== ================== =========
diffractometer        fourc
sample name           LNO_LAO
energy (keV)          10.00337
wavelength (angstrom) 1.23942
calc engine           hkl
mode                  bissector
h                     1.9999956934724616 pseudo
k                     1.999999875673663  pseudo
l                     1.900000040364338  pseudo
omega                 33.891             real
chi                   145.985            real
phi                   48.22875           real
tth                   67.78225           real
===================== ================== =========

[48]:
<pyRestTable.rest_table.Table at 0x7f7e640433a0>

compute the motor positions of the (2 2 1.9) reflection#

Observe that tth ~ 2*omega which is consistent with bissector mode. The (2 2 1.9) reflection is not available in constant_omega mode with omega=0. Need to change modes.

[49]:
fourc.calc.engine.mode = "bissector"
fourc.showConstraints()
===== ========= ========== ======== ====
axis  low_limit high_limit value    fit
===== ========= ========== ======== ====
omega -180.0    180.0      33.891   True
chi   -180.0    180.0      145.985  True
phi   -180.0    180.0      48.22875 True
tth   -180.0    180.0      67.78225 True
===== ========= ========== ======== ====

[49]:
<pyRestTable.rest_table.Table at 0x7f7e668bb700>

The constraint on omega must be removed, then compute the default forward solution.

[50]:
fourc.undoLastConstraints()
print(fourc.forwardSolutionsTable(([2, 2, 1.9],), full=True))
print("\n  Now, just the default solution.")
print(fourc.forwardSolutionsTable(([2, 2, 1.9],)))
=========== ======== ========== ========== ========== =========
(hkl)       solution omega      chi        phi        tth
=========== ======== ========== ========== ========== =========
[2, 2, 1.9] 0        33.89115   145.98503  48.22884   67.78231
[2, 2, 1.9] 1        33.89115   34.01497   -131.77116 67.78231
[2, 2, 1.9] 2        -146.10885 -34.01497  48.22884   67.78231
[2, 2, 1.9] 3        -33.89115  -34.01497  48.22884   -67.78231
[2, 2, 1.9] 4        -146.10885 -145.98503 -131.77116 67.78231
[2, 2, 1.9] 5        -33.89115  -145.98503 -131.77116 -67.78231
=========== ======== ========== ========== ========== =========


  Now, just the default solution.
=========== ======== ======== ========= ======== ========
(hkl)       solution omega    chi       phi      tth
=========== ======== ======== ========= ======== ========
[2, 2, 1.9] 0        33.89115 145.98503 48.22884 67.78231
=========== ======== ======== ========= ======== ========

Apply constraints such that all motors are not negative, then recompute and show:

[51]:
my_constraints = {
    # axis: Constraint(lo_limit, hi_limit, value, fit)
    "omega": Constraint(0, 180, 0, True),
    "chi": Constraint(0, 180, 0, True),
    "phi": Constraint(0, 180, 0, True),
    "tth": Constraint(0, 180, 0, True),
}
fourc.applyConstraints(my_constraints)
fourc.showConstraints()
===== ========= ========== ===== ====
axis  low_limit high_limit value fit
===== ========= ========== ===== ====
omega 0.0       180.0      0.0   True
chi   0.0       180.0      0.0   True
phi   0.0       180.0      0.0   True
tth   0.0       180.0      0.0   True
===== ========= ========== ===== ====

[51]:
<pyRestTable.rest_table.Table at 0x7f7e668c6250>

Summarize the diffractometer settings.

[52]:
fourc.wh()
===================== ================== =========
term                  value              axis_type
===================== ================== =========
diffractometer        fourc
sample name           LNO_LAO
energy (keV)          10.00337
wavelength (angstrom) 1.23942
calc engine           hkl
mode                  bissector
h                     1.9999956934724616 pseudo
k                     1.999999875673663  pseudo
l                     1.900000040364338  pseudo
omega                 33.891             real
chi                   145.985            real
phi                   48.22875           real
tth                   67.78225           real
===================== ================== =========

[52]:
<pyRestTable.rest_table.Table at 0x7f7e668bbf70>

Print the default solution for (2 2 1.9).

[53]:
print(
    fourc.forwardSolutionsTable(
        (
            [2, 2, 1.9],
        )
    )
)
=========== ======== ======== ========= ======== ========
(hkl)       solution omega    chi       phi      tth
=========== ======== ======== ========= ======== ========
[2, 2, 1.9] 0        33.89115 145.98503 48.22884 67.78231
=========== ======== ======== ========= ======== ========

These values match exactly the values from the SPEC data file.

sample (2k2) scan, near k=2#

Use the fourc object as the detector for the scans. Plotting is not necessary.

[54]:
from instrument.framework import bec
bec.disable_plots()

Next, scan (2k2) near k=2:

[55]:
fourc.move(2, 2, 2)
RE(bp.rel_scan([fourc], fourc.k, -0.2, 0.2, 9))


Transient Scan ID: 1     Time: 2020-12-16 17:24:56
Persistent Unique Scan ID: '9a48a80e-deaa-4c5f-bd9c-2d4fe8d79c5e'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|   seq_num |       time |    fourc_k |    fourc_h |    fourc_l | fourc_omega |  fourc_chi |  fourc_phi |  fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|         1 | 17:24:56.4 |      1.800 |      2.000 |      2.000 |      33.274 |    143.277 |     45.204 |     66.547 |
|         2 | 17:24:56.4 |      1.850 |      2.000 |      2.000 |      33.578 |    143.613 |     45.988 |     67.157 |
|         3 | 17:24:56.4 |      1.900 |      2.000 |      2.000 |      33.890 |    143.949 |     46.753 |     67.780 |
|         4 | 17:24:56.4 |      1.950 |      2.000 |      2.000 |      34.208 |    144.284 |     47.499 |     68.417 |
|         5 | 17:24:56.4 |      2.000 |      2.000 |      2.000 |      34.534 |    144.617 |     48.227 |     69.067 |
|         6 | 17:24:56.4 |      2.050 |      2.000 |      2.000 |      34.866 |    144.950 |     48.936 |     69.732 |
|         7 | 17:24:56.5 |      2.100 |      2.000 |      2.000 |      35.205 |    145.281 |     49.628 |     70.409 |
|         8 | 17:24:56.5 |      2.150 |      2.000 |      2.000 |      35.550 |    145.610 |     50.303 |     71.100 |
|         9 | 17:24:56.5 |      2.200 |      2.000 |      2.000 |      35.902 |    145.937 |     50.962 |     71.804 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator rel_scan ['9a48a80e'] (scan num: 1)
[55]:
('9a48a80e-deaa-4c5f-bd9c-2d4fe8d79c5e',)

Example - epitaxial thin film on substrate#

Found an example on the web from Rigaku. The sample is an epitaxial thin film of Mn3O4 on MgO substrate.

ref: http://www.rigaku.com/downloads/journal/Vol16.1.1999/cguide.pdf

Condense all the setup steps into one cell here.

[56]:
fourc = FourCircleDiffractometer('', name='fourc')
fourc.calc.new_sample('Mn3O4/MgO thin film',
    lattice=Lattice(
        a=5.72, b=5.72, c=9.5,
        alpha=90.0, beta=90.0, gamma=90.0))
fourc.calc.wavelength = 12.3984244 / 8.04   # Cu Kalpha
fourc.calc.sample.compute_UB(
    fourc.calc.sample.add_reflection(
        -1.998, -1.994, 4.011,
        position=fourc.calc.Position(
            tth=80.8769, omega=40.6148, chi=0.647, phi=-121.717)),
    fourc.calc.sample.add_reflection(
        -0.997, -0.997, 2.009,
        position=fourc.calc.Position(
            tth=28.695, omega=14.4651, chi=-48.8860, phi=-88.758))
)
[56]:
1

Apply some constraints

[57]:
my_constraints = {
    # axis: Constraint(lo_limit, hi_limit, value, fit)
    "omega": Constraint(-120, 120, 0, True),
    "chi": Constraint(-120, 120, 0, True),
    # "phi": Constraint(-180, 180, 0, True),
    "tth": Constraint(-5, 120, 0, True),
}
fourc.applyConstraints(my_constraints)
fourc.showConstraints()
===== =================== ================== ===== ====
axis  low_limit           high_limit         value fit
===== =================== ================== ===== ====
omega -119.99999999999999 119.99999999999999 0.0   True
chi   -119.99999999999999 119.99999999999999 0.0   True
phi   -180.0              180.0              0.0   True
tth   -5.0                119.99999999999999 0.0   True
===== =================== ================== ===== ====

[57]:
<pyRestTable.rest_table.Table at 0x7f7e668f2ca0>
[58]:
r = []  # list of reflections - (hkl) tuples
r.append((-3,0,5))
r.append((-2,-2,4))
r.append((-2,1,1))
r.append((-1,-1,2))
r.append((0,3,.5))
r.append((0,3,1))
r.append((0,3,1.5))
print(fourc.forwardSolutionsTable(r))
=========== ======== ======== ======== ========== ========
(hkl)       solution omega    chi      phi        tth
=========== ======== ======== ======== ========== ========
(-3, 0, 5)  0        34.95305 16.93669 -92.56666  69.90610
(-2, -2, 4) 0        30.05045 0.68945  -121.67536 60.10091
(-2, 1, 1)  0        18.18911 48.65405 -68.07899  36.37821
(-1, -1, 2) 0        14.50007 0.68945  -121.67536 29.00014
(0, 3, 0.5) 0        23.98052 14.97372 -2.28512   47.96104
(0, 3, 1)   0        24.35942 12.23302 -7.33156   48.71883
(0, 3, 1.5) 0        24.98135 9.51049  -12.08783  49.96270
=========== ======== ======== ======== ========== ========

[59]:
fourc.wh()
===================== =================== =========
term                  value               axis_type
===================== =================== =========
diffractometer        fourc
sample name           Mn3O4/MgO thin film
energy (keV)          8.04000
wavelength (angstrom) 1.54209
calc engine           hkl
mode                  bissector
h                     0.0                 pseudo
k                     0.0                 pseudo
l                     0.0                 pseudo
omega                 0                   real
chi                   0                   real
phi                   0                   real
tth                   0                   real
===================== =================== =========

[59]:
<pyRestTable.rest_table.Table at 0x7f7e668bb4c0>

Scan along the (0kl) direction (down in k, up in l).

[60]:
RE(bp.scan([fourc], fourc.k, 3.2, 2.8, fourc.l, 0.5, 1.5, 11))


Transient Scan ID: 2     Time: 2020-12-16 17:24:57
Persistent Unique Scan ID: 'b5cff878-570f-4305-b5a1-db69f171b005'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|   seq_num |       time |    fourc_k |    fourc_l |    fourc_h | fourc_omega |  fourc_chi |  fourc_phi |  fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|         1 | 17:24:57.4 |      3.200 |      0.500 |      0.000 |      25.675 |     15.144 |     -1.961 |     51.349 |
|         2 | 17:24:57.4 |      3.160 |      0.600 |     -0.000 |      25.387 |     14.594 |     -3.003 |     50.775 |
|         3 | 17:24:57.4 |      3.120 |      0.700 |      0.000 |      25.112 |     14.028 |     -4.062 |     50.224 |
|         4 | 17:24:57.5 |      3.080 |      0.800 |     -0.000 |      24.849 |     13.446 |     -5.136 |     49.698 |
|         5 | 17:24:57.5 |      3.040 |      0.900 |     -0.000 |      24.598 |     12.847 |     -6.226 |     49.196 |
|         6 | 17:24:57.5 |      3.000 |      1.000 |     -0.000 |      24.359 |     12.233 |     -7.332 |     48.719 |
|         7 | 17:24:57.5 |      2.960 |      1.100 |      0.000 |      24.134 |     11.603 |     -8.452 |     48.268 |
|         8 | 17:24:57.5 |      2.920 |      1.200 |     -0.000 |      23.921 |     10.958 |     -9.586 |     47.843 |
|         9 | 17:24:57.5 |      2.880 |      1.300 |      0.000 |      23.722 |     10.298 |    -10.733 |     47.444 |
|        10 | 17:24:57.5 |      2.840 |      1.400 |     -0.000 |      23.537 |      9.624 |    -11.893 |     47.073 |
|        11 | 17:24:57.5 |      2.800 |      1.500 |      0.000 |      23.365 |      8.936 |    -13.066 |     46.730 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator scan ['b5cff878'] (scan num: 2)
[60]:
('b5cff878-570f-4305-b5a1-db69f171b005',)

Scan again, keeping phi=0.

[61]:
fourc.calc.engine.mode = 'constant_phi'
my_constraints = {
    # add this one constraint
    "phi": Constraint(-180, 180, 0, False),
}
fourc.applyConstraints(my_constraints)
fourc.showConstraints()
print("\n Solutions:")
print(fourc.forwardSolutionsTable((
    [0, 3.2, 0.5],
    [0, 2.8, 1.5],
)))

# VERY IMPORTANT, otherwise phi will stay at current position (-13.066 from previous scan)
%mov fourc.phi 0

RE(bp.scan([fourc], fourc.k, 3.2, 2.8, fourc.l, 0.5, 1.5, 11))
===== =================== ================== ================== =====
axis  low_limit           high_limit         value              fit
===== =================== ================== ================== =====
omega -119.99999999999999 119.99999999999999 23.364811487006858 True
chi   -119.99999999999999 119.99999999999999 8.936401718625467  True
phi   -180.0              180.0              0.0                False
tth   -5.0                119.99999999999999 46.729622974013715 True
===== =================== ================== ================== =====


 Solutions:
============= ======== ======== ======== ======= ========
(hkl)         solution omega    chi      phi     tth
============= ======== ======== ======== ======= ========
[0, 3.2, 0.5] 0        23.78144 15.15228 0.00000 51.34916
[0, 2.8, 1.5] 0        10.46066 9.16991  0.00000 46.72962
============= ======== ======== ======== ======= ========



Transient Scan ID: 3     Time: 2020-12-16 17:24:58
Persistent Unique Scan ID: 'dc673022-3c4a-4b78-8a22-72e9a3a15e9c'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|   seq_num |       time |    fourc_k |    fourc_l |    fourc_h | fourc_omega |  fourc_chi |  fourc_phi |  fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|         1 | 17:24:58.0 |      3.200 |      0.500 |     -0.000 |      23.781 |     15.152 |      0.000 |     51.349 |
|         2 | 17:24:58.1 |      3.160 |      0.600 |      0.000 |      22.481 |     14.613 |      0.000 |     50.775 |
|         3 | 17:24:58.1 |      3.120 |      0.700 |     -0.000 |      21.172 |     14.062 |      0.000 |     50.224 |
|         4 | 17:24:58.1 |      3.080 |      0.800 |      0.000 |      19.854 |     13.498 |      0.000 |     49.698 |
|         5 | 17:24:58.1 |      3.040 |      0.900 |     -0.000 |      18.528 |     12.921 |      0.000 |     49.196 |
|         6 | 17:24:58.1 |      3.000 |      1.000 |      0.000 |      17.195 |     12.331 |      0.000 |     48.719 |
|         7 | 17:24:58.2 |      2.960 |      1.100 |      0.000 |      15.856 |     11.727 |      0.000 |     48.268 |
|         8 | 17:24:58.2 |      2.920 |      1.200 |     -0.000 |      14.512 |     11.110 |      0.000 |     47.843 |
|         9 | 17:24:58.2 |      2.880 |      1.300 |     -0.000 |      13.164 |     10.478 |      0.000 |     47.444 |
|        10 | 17:24:58.2 |      2.840 |      1.400 |      0.000 |      11.813 |      9.831 |      0.000 |     47.073 |
|        11 | 17:24:58.2 |      2.800 |      1.500 |     -0.000 |      10.461 |      9.170 |      0.000 |     46.730 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator scan ['dc673022'] (scan num: 3)
[61]:
('dc673022-3c4a-4b78-8a22-72e9a3a15e9c',)