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.

E4CV geometry

Setup the E4CV diffractometer in hklpy2#

The hkl_soleil E4CV geometry is described:

axis

moves

rotation axis

vector

omega

sample

\(-\vec{y}\)

[0 -1 0]

chi

sample

\(\vec{x}\)

[1 0 0]

phi

sample

\(-\vec{y}\)

[0 -1 0]

tth

detector

\(-\vec{y}\)

[0 -1 0]

  • 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 range

  • keep omega in the negative range

  • allow 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#

  1. Check the inverse() (angles -> (hkl)) computation.

  2. 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

cahkl()

The default solution

cahkl_table()

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'}}