hkl_soleil E4CV#
At X-ray synchrotrons, the vertical E4CV geometry is more common (than E4CH, common in labs) due to the polarization of the X-rays.
Setup the E4CV diffractometer in hklpy2#
The hkl_soleil E4CV geometry is described:
axis |
moves |
rotation axis |
vector |
---|---|---|---|
omega |
sample |
\(-\vec{y}\) |
|
chi |
sample |
\(\vec{x}\) |
|
phi |
sample |
\(-\vec{y}\) |
|
tth |
detector |
\(-\vec{y}\) |
|
xrays incident on the $\vec{x}$ direction (1, 0, 0)
Define this diffractometer#
Use the hklpy2 creator()
function to create a diffractometer
object. The diffractometer object will have simulated rotational axes.
We’ll provide the geometry and solver names.
By convention, the name
keyword is the same as the object name.
See the geometry tables for a more complete description of the available diffractometers.
Create the Python diffractometer object (fourc
).
import hklpy2
fourc = hklpy2.creator(name="fourc", geometry="E4CV", solver="hkl_soleil")
Add a sample with a crystal structure#
from hklpy2.user import add_sample, calc_UB, cahkl, cahkl_table, pa, set_diffractometer, setor, wh
set_diffractometer(fourc)
add_sample("silicon", a=hklpy2.SI_LATTICE_PARAMETER)
Sample(name='silicon', lattice=Lattice(a=5.431, system='cubic'))
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.
Diffractometer wavelength#
Set the diffractometer’s X-ray wavelength. This will be used for both reflections. fourc.wavelength
is an
ophyd Signal. Use
its .put()
method.
fourc.wavelength.put(1.54)
Specify the first reflection#
Provide the set of angles that correspond with the reflection’s Miller indices: (hkl)
The setor()
(set orienting reflection) method uses the diffractometer’s wavelength at the time it is called. (To add reflections at different wavelengths, add a wavelength=1.0
keyword argument with the correct value.)
r1 = setor(4, 0, 0, tth=69.0966, omega=-145.451, chi=0, phi=0)
Specify the second reflection#
r2 = setor(0, 4, 0, tth=69.0966, omega=-145.451, chi=90, phi=0)
Compute the UB orientation matrix#
The calc_UB()
method returns the computed UB matrix.
calc_UB(r1, r2)
[[-1.4134285e-05, -1.4134285e-05, -1.156906937382],
[0.0, -1.156906937469, 1.4134285e-05],
[-1.156906937469, 1.73e-10, 1.4134285e-05]]
Report our setup#
pa()
diffractometer='fourc'
HklSolver(name='hkl_soleil', version='5.1.2', geometry='E4CV', engine_name='hkl', mode='bissector')
Sample(name='silicon', lattice=Lattice(a=5.431, system='cubic'))
Reflection(name='r_9ee9', h=4, k=0, l=0)
Reflection(name='r_39f5', h=0, k=4, l=0)
Orienting reflections: ['r_9ee9', 'r_39f5']
U=[[-1.2217305e-05, -1.2217305e-05, -0.999999999851], [0.0, -0.999999999925, 1.2217305e-05], [-0.999999999925, 1.49e-10, 1.2217305e-05]]
UB=[[-1.4134285e-05, -1.4134285e-05, -1.156906937382], [0.0, -1.156906937469, 1.4134285e-05], [-1.156906937469, 1.73e-10, 1.4134285e-05]]
constraint: -180.0 <= omega <= 180.0
constraint: -180.0 <= chi <= 180.0
constraint: -180.0 <= phi <= 180.0
constraint: -180.0 <= tth <= 180.0
h=0, k=0, l=0
wavelength=1.54
omega=0, chi=0, phi=0, tth=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#
keep
tth
in the positive rangekeep
omega
in the negative rangeallow for slight roundoff errors
keep
phi
fixed at zero
First, apply constraints to each of the
rotational motors. Constraints are part of the diffractometer’s core
-level
support.
Note: A constraint does not limit the range of the motor, it constrains
the choice of solutions from the forward()
computation.
fourc.core.constraints["tth"].limits = -0.001, 180
fourc.core.constraints["omega"].limits = (-180, 0.001)
fourc.core.constraints
['-180.0 <= omega <= 0.001', '-180.0 <= chi <= 180.0', '-180.0 <= phi <= 180.0', '-0.001 <= tth <= 180.0']
(400) reflection test#
Check the
inverse()
(angles -> (hkl)) computation.Check the
forward()
((hkl) -> angles) computation.
Check inverse()
at (400)#
To calculate the (hkl) corresponding to a given set of motor angles,
call fourc.inverse()
.
The hkl values are provided as a Python namedtuple structure.
fourc.inverse((-145.451, 0, 0, 69.0966))
Hklpy2DiffractometerPseudoPos(h=3.999916764257, k=0, l=0)
Check forward(400)
#
Compute the angles necessary to position the diffractometer for the given reflection.
Note:
For the forward()
computation, more than one set of angles may be used to reach the same crystal reflection. This test will report the default selection. The default selection (which may be changed through methods described in module :mod:hklpy2.ops
) is the first solution.
function |
returns |
---|---|
|
The default solution |
|
Table of all allowed solutions. |
Before calling forward()
, make sure we are using the desired operations
mode. "bissector"
maintains $\omega = (2\theta) / 2$
fourc.core.solver.mode = "bissector"
Here we print the default solution (the one returned by calling cahkl()
.
cahkl(4, 0, 0)
Hklpy2DiffractometerRealPos(omega=-145.450879077739, chi=0, phi=0.000699999914, tth=69.098241844523)
Note: cahkl()
is a shortcut to fourc.forward()
.
fourc.forward(4, 0, 0)
Hklpy2DiffractometerRealPos(omega=-145.450879077739, chi=0, phi=0.000699999914, tth=69.098241844523)
Show the table of all forward()
solutions for $(4\ 0\ 0)$ and $(0\ 4\ 0)$ allowed by the current constraints. Since this function accepts a list of $hkl$ reflections, extra Python syntax is applied.
cahkl_table((4, 0, 0), (0, 4, 0))
======= = ========= ======= ========= =======
(hkl) # omega chi phi tth
======= = ========= ======= ========= =======
(4 0 0) 1 -145.4509 0 0.0007 69.0982
(4 0 0) 2 -34.5491 0 -110.9011 69.0982
(4 0 0) 3 -34.5491 -180.0 -69.0975 69.0982
(4 0 0) 4 -145.4509 -180.0 -179.9993 69.0982
(4 0 0) 5 -34.5491 180.0 -69.0975 69.0982
(4 0 0) 6 -145.4509 180.0 -179.9993 69.0982
(0 4 0) 1 -145.4509 89.9993 89.9997 69.0982
(0 4 0) 2 -145.4509 89.9993 90.0003 69.0982
(0 4 0) 3 -145.4509 90.0007 -89.9997 69.0982
(0 4 0) 4 -145.4509 90.0007 -90.0003 69.0982
======= = ========= ======= ========= =======
(040) reflection test#
Repeat the inverse
and forward
calculations for the
second orientation reflection.
Check inverse()
at (040)#
fourc.inverse(-145.451, 90, 0, 69.0966)
Hklpy2DiffractometerPseudoPos(h=5.97e-10, k=3.999916764257, l=0)
Check forward(040)
#
fourc.forward(0, 4, 0)
Hklpy2DiffractometerRealPos(omega=-145.450879003913, chi=89.999299941632, phi=89.980126517349, tth=69.098241992175)
Scan in reciprocal space using Bluesky#
To scan with Bluesky, we need more setup.
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
bec = BestEffortCallback()
bec.disable_plots()
cat = databroker.temp().v2
sd = SupplementalData()
RE = RunEngine({})
RE.md = {}
RE.preprocessors.append(sd)
RE.subscribe(cat.v1.insert)
RE.subscribe(bec)
1
Setup the RE
to save the fourc
Configuration
with every run.
crw = hklpy2.ConfigurationRunWrapper(fourc)
RE.preprocessors.append(crw.wrapper)
(h00) scan near (400)#
In this example, we have no detector. Still, we add the diffractometer object in the detector list so that the hkl and motor positions will appear as columns in the table.
fourc.move(4, 0, 0)
RE(bp.scan([fourc], fourc.h, 3.9, 4.1, 5))
Transient Scan ID: 1 Time: 2025-03-26 18:55:00
Persistent Unique Scan ID: '8ccf2f40-03d0-4c52-9c3e-5f2b2d0bdfcb'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| seq_num | time | fourc_h | fourc_k | fourc_l | fourc_omega | fourc_chi | fourc_phi | fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| 1 | 18:55:00.2 | 3.900 | 0.000 | 0.000 | -33.569 | 0 | -112.862 | 67.137 |
| 2 | 18:55:00.2 | 3.950 | 0.000 | 0.000 | -34.057 | 0 | -111.884 | 68.115 |
| 3 | 18:55:00.2 | 4.000 | 0.000 | 0.000 | -34.549 | 0 | -110.901 | 69.098 |
| 4 | 18:55:00.2 | 4.050 | 0.000 | 0.000 | -35.044 | 0 | -109.912 | 70.087 |
| 5 | 18:55:00.2 | 4.100 | 0.000 | 0.000 | -35.541 | 0 | -108.917 | 71.083 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator scan ['8ccf2f40'] (scan num: 1)
('8ccf2f40-03d0-4c52-9c3e-5f2b2d0bdfcb',)
chi scan from (400) to (040)#
If we do this with $\omega=-145.4500$ and $2\theta=69.0985$, this will be a scan between the two orientation reflections.
Use %mov
(IPython magic command) to move both motors at the same time.
# same as orientation reflections
RE(bps.mv(fourc.omega,-145.4500, fourc.tth,69.0985))
RE(bp.scan([fourc], fourc.chi, 0, 90, 10))
Transient Scan ID: 2 Time: 2025-03-26 18:55:00
Persistent Unique Scan ID: 'c556ed70-1fa3-4f1d-899a-0fa025865276'
New stream: 'primary'
+-----------+------------+------------+------------+------------+------------+-------------+------------+------------+
| seq_num | time | fourc_chi | fourc_h | fourc_k | fourc_l | fourc_omega | fourc_phi | fourc_tth |
+-----------+------------+------------+------------+------------+------------+-------------+------------+------------+
| 1 | 18:55:00.4 | 0.000 | -1.297 | -0.000 | -3.784 | -145.450 | -108.917 | 69.099 |
| 2 | 18:55:00.4 | 10.000 | -1.277 | 0.695 | -3.727 | -145.450 | -108.917 | 69.099 |
| 3 | 18:55:00.4 | 20.000 | -1.219 | 1.368 | -3.556 | -145.450 | -108.917 | 69.099 |
| 4 | 18:55:00.4 | 30.000 | -1.123 | 2.000 | -3.277 | -145.450 | -108.917 | 69.099 |
| 5 | 18:55:00.4 | 40.000 | -0.993 | 2.571 | -2.899 | -145.450 | -108.917 | 69.099 |
| 6 | 18:55:00.4 | 50.000 | -0.834 | 3.064 | -2.432 | -145.450 | -108.917 | 69.099 |
| 7 | 18:55:00.4 | 60.000 | -0.648 | 3.464 | -1.892 | -145.450 | -108.917 | 69.099 |
| 8 | 18:55:00.4 | 70.000 | -0.443 | 3.759 | -1.294 | -145.450 | -108.917 | 69.099 |
| 9 | 18:55:00.4 | 80.000 | -0.225 | 3.939 | -0.657 | -145.450 | -108.917 | 69.099 |
| 10 | 18:55:00.4 | 90.000 | 0.000 | 4.000 | -0.000 | -145.450 | -108.917 | 69.099 |
+-----------+------------+------------+------------+------------+------------+-------------+------------+------------+
generator scan ['c556ed70'] (scan num: 2)
('c556ed70-1fa3-4f1d-899a-0fa025865276',)
(0k0) scan near (040)#
fourc.move(0, 4, 0)
RE(bp.scan([fourc], fourc.k, 3.9, 4.1, 5))
Transient Scan ID: 3 Time: 2025-03-26 18:55:00
Persistent Unique Scan ID: 'ef28fef9-5b67-4d82-b9bd-a92cf78249ef'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| seq_num | time | fourc_k | fourc_h | fourc_l | fourc_omega | fourc_chi | fourc_phi | fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| 1 | 18:55:00.6 | 3.900 | 0.000 | -0.000 | -146.431 | 90.001 | -90.002 | 67.137 |
| 2 | 18:55:00.6 | 3.950 | 0.000 | -0.000 | -145.943 | 90.001 | -90.001 | 68.115 |
| 3 | 18:55:00.6 | 4.000 | 0.000 | -0.000 | -145.451 | 90.001 | -90.001 | 69.098 |
| 4 | 18:55:00.6 | 4.050 | 0.000 | 0.000 | -144.956 | 90.001 | -90.001 | 70.087 |
| 5 | 18:55:00.7 | 4.100 | 0.000 | -0.000 | -144.459 | 90.001 | -90.000 | 71.083 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator scan ['ef28fef9'] (scan num: 3)
('ef28fef9-5b67-4d82-b9bd-a92cf78249ef',)
(hk0) scan near (440)#
fourc.move(4, 4, 0)
RE(bp.scan([fourc], fourc.h, 3.9, 4.1, fourc.k, 3.9, 4.1, 5))
Transient Scan ID: 4 Time: 2025-03-26 18:55:00
Persistent Unique Scan ID: '49a6588c-636b-40a2-a73e-003c6062933b'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| seq_num | time | fourc_h | fourc_k | fourc_l | fourc_omega | fourc_chi | fourc_phi | fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| 1 | 18:55:00.8 | 3.900 | 3.900 | -0.000 | -128.559 | 135.000 | -179.999 | 102.882 |
| 2 | 18:55:00.8 | 3.950 | 3.950 | 0.000 | -127.628 | 135.000 | -179.999 | 104.744 |
| 3 | 18:55:00.8 | 4.000 | 4.000 | 0.000 | -126.677 | 135.000 | -179.999 | 106.647 |
| 4 | 18:55:00.9 | 4.050 | 4.050 | -0.000 | -125.704 | 135.000 | -179.999 | 108.592 |
| 5 | 18:55:00.9 | 4.100 | 4.100 | -0.000 | -124.708 | 135.000 | -179.999 | 110.585 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator scan ['49a6588c'] (scan num: 4)
('49a6588c-636b-40a2-a73e-003c6062933b',)
Move to the (440) reflection.
fourc.move((4, 4, 0))
print(f"{fourc.position = }")
fourc.position = Hklpy2DiffractometerPseudoPos(h=3.999999995224, k=3.999999999806, l=-2.094e-09)
Repeat the same scan about the (440) but use relative positions.
RE(bp.rel_scan([fourc], fourc.h, -0.1, 0.1, fourc.k, -0.1, 0.1, 5))
Transient Scan ID: 5 Time: 2025-03-26 18:55:01
Persistent Unique Scan ID: 'bef1c41b-af46-4131-b8bd-d58d86e719c4'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| seq_num | time | fourc_h | fourc_k | fourc_l | fourc_omega | fourc_chi | fourc_phi | fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| 1 | 18:55:01.1 | 3.900 | 3.900 | -0.000 | -128.559 | 135.000 | -179.999 | 102.882 |
| 2 | 18:55:01.1 | 3.950 | 3.950 | -0.000 | -127.628 | 135.000 | -179.999 | 104.744 |
| 3 | 18:55:01.1 | 4.000 | 4.000 | 0.000 | -126.677 | 135.000 | -179.999 | 106.647 |
| 4 | 18:55:01.1 | 4.050 | 4.050 | -0.000 | -125.704 | 135.000 | -179.999 | 108.592 |
| 5 | 18:55:01.1 | 4.100 | 4.100 | -0.000 | -124.708 | 135.000 | -179.999 | 110.585 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator rel_scan ['bef1c41b'] (scan num: 5)
('bef1c41b-af46-4131-b8bd-d58d86e719c4',)
scan $(h40)$ with $\varphi=0$#
fourc.core.solver.mode = "constant_phi"
fourc.phi.move(0, wait=True)
wh()
h=-4.0, k=4.0, l=0
wavelength=1.54
omega=-126.6767, chi=135.0, phi=0, tth=106.6465
RE(bp.rel_scan([fourc], fourc.h, -0.1, 0.1, fourc.k, -0.1, 0.1, 5))
Transient Scan ID: 6 Time: 2025-03-26 18:55:01
Persistent Unique Scan ID: 'cb7e8801-bd88-4b51-bdb0-85d2585ff7fb'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| seq_num | time | fourc_h | fourc_k | fourc_l | fourc_omega | fourc_chi | fourc_phi | fourc_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
| 1 | 18:55:01.3 | -4.100 | 3.900 | -0.000 | -126.653 | 136.432 | 0 | 106.695 |
| 2 | 18:55:01.3 | -4.050 | 3.950 | 0.000 | -126.671 | 135.716 | 0 | 106.659 |
| 3 | 18:55:01.3 | -4.000 | 4.000 | 0.000 | -126.677 | 135.000 | 0 | 106.647 |
| 4 | 18:55:01.4 | -3.950 | 4.050 | -0.000 | -126.671 | 134.284 | 0 | 106.659 |
| 5 | 18:55:01.4 | -3.900 | 4.100 | -0.000 | -126.653 | 133.568 | 0 | 106.695 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator rel_scan ['cb7e8801'] (scan num: 6)
('cb7e8801-bd88-4b51-bdb0-85d2585ff7fb',)
Show the configuration#
Print the diffractometer configuration that was saved with the run.
cat.v2[-1].metadata["start"]["diffractometers"]["fourc"]
{'_header': {'datetime': '2025-03-26 18:55:01.320422',
'hklpy2_version': '0.0.28.dev104+g1a2d5d7.d20250326',
'python_class': 'Hklpy2Diffractometer',
'source_type': 'X-ray',
'energy_units': 'keV',
'energy': 8.050921976530415,
'wavelength_units': 'angstrom',
'wavelength': 1.54},
'name': 'fourc',
'axes': {'pseudo_axes': ['h', 'k', 'l'],
'real_axes': ['omega', 'chi', 'phi', 'tth'],
'axes_xref': {'h': 'h',
'k': 'k',
'l': 'l',
'omega': 'omega',
'chi': 'chi',
'phi': 'phi',
'tth': 'tth'},
'extra_axes': {}},
'sample_name': 'silicon',
'samples': {'sample': {'name': 'sample',
'lattice': {'a': 1,
'b': 1,
'c': 1,
'alpha': 90.0,
'beta': 90.0,
'gamma': 90.0},
'reflections': {},
'reflections_order': [],
'U': [[1, 0, 0], [0, 1, 0], [0, 0, 1]],
'UB': [[6.283185307179586, 0.0, 0.0],
[0.0, 6.283185307179586, 0.0],
[0.0, 0.0, 6.283185307179586]],
'digits': 4},
'silicon': {'name': 'silicon',
'lattice': {'a': 5.431020511,
'b': 5.431020511,
'c': 5.431020511,
'alpha': 90,
'beta': 90,
'gamma': 90},
'reflections': {'r_9ee9': {'name': 'r_9ee9',
'geometry': 'E4CV',
'pseudos': {'h': 4, 'k': 0, 'l': 0},
'reals': {'omega': -145.451, 'chi': 0, 'phi': 0, 'tth': 69.0966},
'wavelength': 1.54,
'digits': 4},
'r_39f5': {'name': 'r_39f5',
'geometry': 'E4CV',
'pseudos': {'h': 0, 'k': 4, 'l': 0},
'reals': {'omega': -145.451, 'chi': 90, 'phi': 0, 'tth': 69.0966},
'wavelength': 1.54,
'digits': 4}},
'reflections_order': ['r_9ee9', 'r_39f5'],
'U': [[-1.2217305e-05, -1.2217305e-05, -0.999999999851],
[0.0, -0.999999999925, 1.2217305e-05],
[-0.999999999925, 1.49e-10, 1.2217305e-05]],
'UB': [[-1.4134285e-05, -1.4134285e-05, -1.156906937382],
[0.0, -1.156906937469, 1.4134285e-05],
[-1.156906937469, 1.73e-10, 1.4134285e-05]],
'digits': 4}},
'constraints': {'omega': {'label': 'omega',
'low_limit': -180.0,
'high_limit': 0.001,
'class': 'LimitsConstraint'},
'chi': {'label': 'chi',
'low_limit': -180.0,
'high_limit': 180.0,
'class': 'LimitsConstraint'},
'phi': {'label': 'phi',
'low_limit': -180.0,
'high_limit': 180.0,
'class': 'LimitsConstraint'},
'tth': {'label': 'tth',
'low_limit': -0.001,
'high_limit': 180.0,
'class': 'LimitsConstraint'}},
'solver': {'name': 'hkl_soleil',
'description': "HklSolver(name='hkl_soleil', version='5.1.2', geometry='E4CV', engine_name='hkl', mode='constant_phi')",
'geometry': 'E4CV',
'real_axes': ['omega', 'chi', 'phi', 'tth'],
'version': '5.1.2',
'engine': 'hkl'}}