hkl_soleil K4CV#

The kappa ($\kappa$) diffractometer geometry replaces the $\chi$-ring on an Eulerian 4-circle diffractometer with a $\kappa$ stage which holds the phi stage. The $\kappa$ stage is tilted at angle $\alpha$ (typically 50 degrees) from the $\omega$ stage.

K4CV geometry

This is the hkl_soleil K4CV geometry:

axis

moves

rotation axis

vector

komega

sample

\(-\vec{y}\)

[0 -1 0]

kappa

sample

\(\vec{x}\)

[0 -0.6428 -0.7660]

kphi

sample

\(-\vec{y}\)

[0 -1 0]

tth

detector

\(-\vec{y}\)

[0 -1 0]

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

See: hkl_soleil documentation for more details.

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 (k4cv). We choose this name to avoid any confusion with the diffractometer’s kappa axis.

import hklpy2

k4cv = hklpy2.creator(name="k4cv", geometry="K4CV", 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(k4cv)
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. k4cv.wavelength is an ophyd Signal. Use its .put() method.

k4cv.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, komega=55.4507, kappa=0, kphi=-90)

Specify the second reflection#

r2 = setor(0, 4, 0, tth=-69.0966, komega=-1.5950, kappa=134.7658, kphi=123.3554)

Compute the UB orientation matrix#

The calc_UB() method returns the computed UB matrix.

calc_UB(r1, r2)
[[2.0191835e-05, -8.3752177e-05, -1.156906934347],
 [0.0, -1.156906934523, 8.3752177e-05],
 [-1.156906937379, -1.462e-09, -2.0191835e-05]]

Report our setup#

pa()
diffractometer='k4cv'
HklSolver(name='hkl_soleil', version='5.1.2', geometry='K4CV', engine_name='hkl', mode='bissector')
Sample(name='silicon', lattice=Lattice(a=5.431, system='cubic'))
Reflection(name='r_7aba', h=4, k=0, l=0)
Reflection(name='r_890a', h=0, k=4, l=0)
Orienting reflections: ['r_7aba', 'r_890a']
U=[[1.7453293e-05, -7.2393184e-05, -0.999999997227], [0.0, -0.99999999738, 7.2393184e-05], [-0.999999999848, -1.263e-09, -1.7453292e-05]]
UB=[[2.0191835e-05, -8.3752177e-05, -1.156906934347], [0.0, -1.156906934523, 8.3752177e-05], [-1.156906937379, -1.462e-09, -2.0191835e-05]]
constraint: -180.0 <= komega <= 180.0
constraint: -180.0 <= kappa <= 180.0
constraint: -180.0 <= kphi <= 180.0
constraint: -180.0 <= tth <= 180.0
h=0, k=0, l=0
wavelength=1.54
komega=0, kappa=0, kphi=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 one of the motors#

  • keep kphi in the negative range

  • allow for slight roundoff errors

First, apply constraints to the kphi rotational motor. 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.


k4cv.core.constraints["kphi"].limits = (-180, 0.001)
k4cv.core.constraints
['-180.0 <= komega <= 180.0', '-180.0 <= kappa <= 180.0', '-180.0 <= kphi <= 0.001', '-180.0 <= 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 k4cv.inverse().

The hkl values are provided as a Python namedtuple structure.

print(f"{k4cv.real_axis_names=}")
# Specify values, by correct order of names.
k4cv.inverse(55.4507, 0, -90, -69.0966)
k4cv.real_axis_names=['komega', 'kappa', 'kphi', 'tth']
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$

k4cv.core.solver.mode = "bissector"

Here we print the default solution (the one returned by calling cahkl().

cahkl(4, 0, 0)
Hklpy2DiffractometerRealPos(komega=55.450879077705, kappa=0, kphi=-90.000999999996, tth=-69.098241844591)

Note: cahkl() is a shortcut to k4cv.forward().

k4cv.forward(4, 0, 0)
Hklpy2DiffractometerRealPos(komega=55.450879077705, kappa=0, kphi=-90.000999999996, tth=-69.098241844591)

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)   # komega   kappa kphi    tth     
======= = ======== ===== ======= ========
(4 0 0) 1 55.4509  0     -90.001 -69.0982
(4 0 0) 2 -55.4509 0     -90.001 69.0982 
======= = ======== ===== ======= ========

(040) reflection test#

Repeat the inverse and forward calculations for the second orientation reflection.

Check inverse() at (040)#

k4cv.inverse(-1.5950, 134.7568, 123.3554, -69.0966)
Hklpy2DiffractometerPseudoPos(h=-6.8202278e-05, k=3.99991675492, l=-0.000264659394)

Check forward(040)#

k4cv.forward(0, 4, 0)
Hklpy2DiffractometerRealPos(komega=-1.587888254116, kappa=134.745974526972, kphi=-57.039768306859, tth=-69.098241844579)

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 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 k4cv Configuration with every run.

crw = hklpy2.ConfigurationRunWrapper(k4cv)
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.

k4cv.move(4, 0, 0)
wh()
RE(bp.scan([k4cv], k4cv.h, 3.9, 4.1, 5))
h=4.0, k=0, l=0
wavelength=1.54
komega=55.4509, kappa=0, kphi=-90.001, tth=-69.0982


Transient Scan ID: 1     Time: 2025-03-26 18:56:21
Persistent Unique Scan ID: 'ba847763-7708-4642-bb7f-35a0c2ddaedb'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|   seq_num |       time |     k4cv_h |     k4cv_k |     k4cv_l | k4cv_komega | k4cv_kappa |  k4cv_kphi |   k4cv_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|         1 | 18:56:21.3 |      3.900 |      0.000 |      0.000 |      56.431 |      0.000 |    -90.001 |    -67.137 |
|         2 | 18:56:21.4 |      3.950 |     -0.000 |     -0.000 |      55.943 |      0.000 |    -90.001 |    -68.115 |
|         3 | 18:56:21.4 |      4.000 |     -0.000 |     -0.000 |      55.451 |     -0.000 |    -90.001 |    -69.098 |
|         4 | 18:56:21.4 |      4.050 |      0.000 |      0.000 |      54.956 |      0.000 |    -90.001 |    -70.087 |
|         5 | 18:56:21.5 |      4.100 |      0.000 |     -0.000 |      54.459 |      0.000 |    -90.001 |    -71.083 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator scan ['ba847763'] (scan num: 1)
('ba847763-7708-4642-bb7f-35a0c2ddaedb',)

(hk0) scan near (440)#

Scan between the two orientation reflections. Need to keep $\varphi\ge0$ to avoid big jumps during the scan.

k4cv.core.constraints["kphi"].limits = -0.001, 180

k4cv.move(4, 4, 0)
RE(bp.rel_scan([k4cv], k4cv.h, -0.2, 0.2, k4cv.k, 0.2, -0.2, 10))
Transient Scan ID: 2     Time: 2025-03-26 18:56:21
Persistent Unique Scan ID: '4e10ba64-f866-46ea-8668-1260af4ad435'
New stream: 'primary'
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|   seq_num |       time |     k4cv_h |     k4cv_k |     k4cv_l | k4cv_komega | k4cv_kappa |  k4cv_kphi |   k4cv_tth |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
|         1 | 18:56:21.6 |      3.800 |      4.200 |      0.000 |    -121.557 |    -63.947 |    111.866 |   -106.839 |
|         2 | 18:56:21.6 |      3.844 |      4.156 |      0.000 |    -121.861 |    -63.054 |    111.524 |   -106.763 |
|         3 | 18:56:21.6 |      3.889 |      4.111 |      0.000 |    -122.173 |    -62.163 |    111.183 |   -106.706 |
|         4 | 18:56:21.6 |      3.933 |      4.067 |      0.000 |    -122.492 |    -61.273 |    110.845 |   -106.668 |
|         5 | 18:56:21.6 |      3.978 |      4.022 |      0.000 |    -122.819 |    -60.385 |    110.509 |   -106.649 |
|         6 | 18:56:21.7 |      4.022 |      3.978 |      0.000 |    -123.152 |    -59.499 |    110.175 |   -106.649 |
|         7 | 18:56:21.7 |      4.067 |      3.933 |      0.000 |    -123.493 |    -58.614 |    109.844 |   -106.668 |
|         8 | 18:56:21.7 |      4.111 |      3.889 |      0.000 |    -123.842 |    -57.732 |    109.514 |   -106.706 |
|         9 | 18:56:21.7 |      4.156 |      3.844 |      0.000 |    -124.197 |    -56.852 |    109.187 |   -106.763 |
|        10 | 18:56:21.7 |      4.200 |      3.800 |      0.000 |    -124.559 |    -55.975 |    108.863 |   -106.839 |
+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+
generator rel_scan ['4e10ba64'] (scan num: 2)
('4e10ba64-f866-46ea-8668-1260af4ad435',)