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
whenfit=True
.high_limit (number) : Limit solutions for this axis to no greater than
high_limit
whenfit=True
.value (number) : Calculate with axis =
value
whenfit=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',)