{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# **hkl_soleil** K4CV\n", "\n", "The *kappa* ($\\kappa$) diffractometer {ref}`geometry\n", "` replaces the $\\chi$-ring on an Eulerian 4-circle\n", "diffractometer with a $\\kappa$ stage which holds the *phi* stage. The $\\kappa$\n", "stage is tilted at angle $\\alpha$ (typically 50 degrees) from the $\\omega$\n", "stage.\n", "\n", "\n", "![K4CV geometry](../_static/k4cv.png)\n", "\n", "This is the *hkl_soleil* *K4CV* [geometry](../geometry_tables.rst):\n", "\n", "axis | moves | rotation axis | vector\n", "--- | --- | --- | ---\n", "komega | sample | {math}`-\\vec{y}` | `[0 -1 0]`\n", "kappa | sample | {math}`\\vec{x}` | `[0 -0.6428 -0.7660]`\n", "kphi | sample | {math}`-\\vec{y}` | `[0 -1 0]`\n", "tth | detector | {math}`-\\vec{y}` | `[0 -1 0]`\n", "\n", "* xrays incident on the {math}`\\vec{x}` direction (1, 0, 0)\n", "\n", "See: *hkl_soleil* [documentation](https://people.debian.org/~picca/hkl/hkl.html) for more details." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Define _this_ diffractometer\n", "\n", "Use the **hklpy2** `creator()` function to create a diffractometer\n", "object. The diffractometer object will have simulated rotational axes.\n", "\n", "We'll provide the geometry and solver names.\n", "By convention, the `name` keyword is the same as the object name.\n", "\n", "See the [geometry tables](../geometry_tables.rst) for\n", "a more complete description of the available diffractometers.\n", "\n", "Create the Python diffractometer object (`k4cv`). We choose this name to avoid any confusion with the diffractometer's `kappa` axis." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import hklpy2\n", "\n", "k4cv = hklpy2.creator(name=\"k4cv\", geometry=\"K4CV\", solver=\"hkl_soleil\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Add a sample with a crystal structure" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Sample(name='silicon', lattice=Lattice(a=5.431, system='cubic'))" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from hklpy2.user import add_sample, calc_UB, cahkl, cahkl_table, pa, set_diffractometer, setor, wh\n", "\n", "set_diffractometer(k4cv)\n", "add_sample(\"silicon\", a=hklpy2.SI_LATTICE_PARAMETER)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Setup the UB orientation matrix using *hklpy*\n", "\n", "Define the crystal's orientation on the diffractometer using \n", "the 2-reflection method described by [Busing & Levy, Acta Cryst 22 (1967) 457](https://www.psi.ch/sites/default/files/import/sinq/zebra/PracticalsEN/1967-Busing-Levy-3-4-circle-Acta22.pdf)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Diffractometer wavelength\n", "\n", "Set the diffractometer's X-ray wavelength. This will be used for both reflections. `k4cv.wavelength` is an\n", "ophyd [Signal](https://blueskyproject.io/ophyd/user/reference/signals.html). Use\n", "its `.put()` method." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "k4cv.wavelength.put(1.54)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Specify the first reflection\n", "\n", "Provide the set of angles that correspond with the reflection's Miller indices: (_hkl_)\n", "\n", "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.) " ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "r1 = setor(4, 0, 0, tth=-69.0966, komega=55.4507, kappa=0, kphi=-90)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Specify the second reflection" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "r2 = setor(0, 4, 0, tth=-69.0966, komega=-1.5950, kappa=134.7658, kphi=123.3554)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Compute the *UB* orientation matrix\n", "\n", "The `calc_UB()` method returns the computed **UB** matrix." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[2.0191835e-05, -8.3752177e-05, -1.156906934347],\n", " [0.0, -1.156906934523, 8.3752177e-05],\n", " [-1.156906937379, -1.462e-09, -2.0191835e-05]]" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "calc_UB(r1, r2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Report our setup" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "diffractometer='k4cv'\n", "HklSolver(name='hkl_soleil', version='5.1.2', geometry='K4CV', engine_name='hkl', mode='bissector')\n", "Sample(name='silicon', lattice=Lattice(a=5.431, system='cubic'))\n", "Reflection(name='r_7aba', h=4, k=0, l=0)\n", "Reflection(name='r_890a', h=0, k=4, l=0)\n", "Orienting reflections: ['r_7aba', 'r_890a']\n", "U=[[1.7453293e-05, -7.2393184e-05, -0.999999997227], [0.0, -0.99999999738, 7.2393184e-05], [-0.999999999848, -1.263e-09, -1.7453292e-05]]\n", "UB=[[2.0191835e-05, -8.3752177e-05, -1.156906934347], [0.0, -1.156906934523, 8.3752177e-05], [-1.156906937379, -1.462e-09, -2.0191835e-05]]\n", "constraint: -180.0 <= komega <= 180.0\n", "constraint: -180.0 <= kappa <= 180.0\n", "constraint: -180.0 <= kphi <= 180.0\n", "constraint: -180.0 <= tth <= 180.0\n", "h=0, k=0, l=0\n", "wavelength=1.54\n", "komega=0, kappa=0, kphi=0, tth=0\n" ] } ], "source": [ "pa()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Check the orientation matrix\n", "\n", "Perform checks with `forward()` ($hkl$ to angle) and\n", "`inverse()` (angle to $hkl$) computations to verify the diffractometer\n", "will move to the same positions where the reflections were identified.\n", "\n", "### Constrain one of the motors\n", "\n", "* keep `kphi` in the negative range\n", "* allow for slight roundoff errors\n", "\n", "First, apply [constraints](../concepts/constraints.rst) to the `kphi`\n", "rotational motor. Constraints are part of the diffractometer's `core`-level\n", "support.\n", "\n", "---\n", "\n", "**Note**: A constraint does not limit the range of the motor, it *constrains*\n", "the choice of solutions from the `forward()` computation.\n", "\n", "---" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['-180.0 <= komega <= 180.0', '-180.0 <= kappa <= 180.0', '-180.0 <= kphi <= 0.001', '-180.0 <= tth <= 180.0']" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "k4cv.core.constraints[\"kphi\"].limits = (-180, 0.001)\n", "k4cv.core.constraints" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### (400) reflection test\n", "\n", "1. Check the `inverse()` (angles -> (_hkl_)) computation.\n", "1. Check the `forward()` ((_hkl_) -> angles) computation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Check `inverse()` at (400)\n", "\n", "To calculate the (_hkl_) corresponding to a given set of motor angles,\n", "call `k4cv.inverse()`.\n", "\n", "The _hkl_ values are provided as a Python [namedtuple](https://docs.python.org/3/library/collections.html#collections.namedtuple) structure." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "k4cv.real_axis_names=['komega', 'kappa', 'kphi', 'tth']\n" ] }, { "data": { "text/plain": [ "Hklpy2DiffractometerPseudoPos(h=3.999916764257, k=0, l=0)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "print(f\"{k4cv.real_axis_names=}\")\n", "# Specify values, by correct order of names.\n", "k4cv.inverse(55.4507, 0, -90, -69.0966)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Check `forward(400)`\n", "\n", "Compute the angles necessary to position the diffractometer\n", "for the given reflection.\n", "\n", "---\n", "\n", "**Note**:\n", "\n", "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.\n", "\n", "function | returns\n", "--- | ---\n", "`cahkl()` | The *default* solution\n", "`cahkl_table()` | Table of all allowed solutions.\n", "\n", "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Before calling `forward()`, make sure we are using the desired operations\n", "*mode*. `\"bissector\"` maintains $\\omega = (2\\theta) / 2$" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "k4cv.core.solver.mode = \"bissector\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here we print the *default* solution (the one returned by calling `cahkl()`." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Hklpy2DiffractometerRealPos(komega=55.450879077705, kappa=0, kphi=-90.000999999996, tth=-69.098241844591)" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "cahkl(4, 0, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "**Note**: `cahkl()` is a shortcut to `k4cv.forward()`." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Hklpy2DiffractometerRealPos(komega=55.450879077705, kappa=0, kphi=-90.000999999996, tth=-69.098241844591)" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "k4cv.forward(4, 0, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "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." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "======= = ======== ===== ======= ========\n", "(hkl) # komega kappa kphi tth \n", "======= = ======== ===== ======= ========\n", "(4 0 0) 1 55.4509 0 -90.001 -69.0982\n", "(4 0 0) 2 -55.4509 0 -90.001 69.0982 \n", "======= = ======== ===== ======= ========\n", "\n" ] } ], "source": [ "cahkl_table((4, 0, 0), (0, 4, 0))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### (040) reflection test\n", "\n", "Repeat the `inverse` and `forward` calculations for the\n", "second orientation reflection." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Check `inverse()` at (040)" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Hklpy2DiffractometerPseudoPos(h=-6.8202278e-05, k=3.99991675492, l=-0.000264659394)" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "k4cv.inverse(-1.5950, 134.7568, 123.3554, -69.0966)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Check `forward(040)`" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Hklpy2DiffractometerRealPos(komega=-1.587888254116, kappa=134.745974526972, kphi=-57.039768306859, tth=-69.098241844579)" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "k4cv.forward(0, 4, 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Scan in reciprocal space using Bluesky\n", "\n", "To scan with Bluesky, we need more setup." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "1" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from bluesky import RunEngine\n", "from bluesky import SupplementalData\n", "from bluesky.callbacks.best_effort import BestEffortCallback\n", "import bluesky.plans as bp\n", "import databroker\n", "\n", "bec = BestEffortCallback()\n", "bec.disable_plots()\n", "cat = databroker.temp().v2\n", "sd = SupplementalData()\n", "\n", "RE = RunEngine({})\n", "RE.md = {}\n", "RE.preprocessors.append(sd)\n", "RE.subscribe(cat.v1.insert)\n", "RE.subscribe(bec)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Setup the `RE` to save the `k4cv` {class}`~hklpy2.blocks.configure.Configuration` with every run." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "crw = hklpy2.ConfigurationRunWrapper(k4cv)\n", "RE.preprocessors.append(crw.wrapper)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### (_h00_) scan near (400)\n", "\n", "In this example, we have no detector. Still, we add the diffractometer\n", "object in the detector list so that the _hkl_ and motor positions will appear\n", "as columns in the table." ] }, { "cell_type": "code", "execution_count": 18, "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "h=4.0, k=0, l=0\n", "wavelength=1.54\n", "komega=55.4509, kappa=0, kphi=-90.001, tth=-69.0982\n", "\n", "\n", "Transient Scan ID: 1 Time: 2025-03-26 18:56:21\n", "Persistent Unique Scan ID: 'ba847763-7708-4642-bb7f-35a0c2ddaedb'\n", "New stream: 'primary'\n", "+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+\n", "| seq_num | time | k4cv_h | k4cv_k | k4cv_l | k4cv_komega | k4cv_kappa | k4cv_kphi | k4cv_tth |\n", "+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+\n", "| 1 | 18:56:21.3 | 3.900 | 0.000 | 0.000 | 56.431 | 0.000 | -90.001 | -67.137 |\n", "| 2 | 18:56:21.4 | 3.950 | -0.000 | -0.000 | 55.943 | 0.000 | -90.001 | -68.115 |\n", "| 3 | 18:56:21.4 | 4.000 | -0.000 | -0.000 | 55.451 | -0.000 | -90.001 | -69.098 |\n", "| 4 | 18:56:21.4 | 4.050 | 0.000 | 0.000 | 54.956 | 0.000 | -90.001 | -70.087 |\n", "| 5 | 18:56:21.5 | 4.100 | 0.000 | -0.000 | 54.459 | 0.000 | -90.001 | -71.083 |\n", "+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+\n", "generator scan ['ba847763'] (scan num: 1)\n", "\n", "\n", "\n" ] }, { "data": { "text/plain": [ "('ba847763-7708-4642-bb7f-35a0c2ddaedb',)" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "\n", "k4cv.move(4, 0, 0)\n", "wh()\n", "RE(bp.scan([k4cv], k4cv.h, 3.9, 4.1, 5))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### (_hk0_) scan near (440)\n", "\n", "Scan between the two orientation reflections. Need to keep $\\varphi\\ge0$ to avoid\n", "big jumps during the scan." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Transient Scan ID: 2 Time: 2025-03-26 18:56:21\n", "Persistent Unique Scan ID: '4e10ba64-f866-46ea-8668-1260af4ad435'\n", "New stream: 'primary'\n", "+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+\n", "| seq_num | time | k4cv_h | k4cv_k | k4cv_l | k4cv_komega | k4cv_kappa | k4cv_kphi | k4cv_tth |\n", "+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+\n", "| 1 | 18:56:21.6 | 3.800 | 4.200 | 0.000 | -121.557 | -63.947 | 111.866 | -106.839 |\n", "| 2 | 18:56:21.6 | 3.844 | 4.156 | 0.000 | -121.861 | -63.054 | 111.524 | -106.763 |\n", "| 3 | 18:56:21.6 | 3.889 | 4.111 | 0.000 | -122.173 | -62.163 | 111.183 | -106.706 |\n", "| 4 | 18:56:21.6 | 3.933 | 4.067 | 0.000 | -122.492 | -61.273 | 110.845 | -106.668 |\n", "| 5 | 18:56:21.6 | 3.978 | 4.022 | 0.000 | -122.819 | -60.385 | 110.509 | -106.649 |\n", "| 6 | 18:56:21.7 | 4.022 | 3.978 | 0.000 | -123.152 | -59.499 | 110.175 | -106.649 |\n", "| 7 | 18:56:21.7 | 4.067 | 3.933 | 0.000 | -123.493 | -58.614 | 109.844 | -106.668 |\n", "| 8 | 18:56:21.7 | 4.111 | 3.889 | 0.000 | -123.842 | -57.732 | 109.514 | -106.706 |\n", "| 9 | 18:56:21.7 | 4.156 | 3.844 | 0.000 | -124.197 | -56.852 | 109.187 | -106.763 |\n", "| 10 | 18:56:21.7 | 4.200 | 3.800 | 0.000 | -124.559 | -55.975 | 108.863 | -106.839 |\n", "+-----------+------------+------------+------------+------------+-------------+------------+------------+------------+\n", "generator rel_scan ['4e10ba64'] (scan num: 2)\n", "\n", "\n", "\n" ] }, { "data": { "text/plain": [ "('4e10ba64-f866-46ea-8668-1260af4ad435',)" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "k4cv.core.constraints[\"kphi\"].limits = -0.001, 180\n", "\n", "k4cv.move(4, 4, 0)\n", "RE(bp.rel_scan([k4cv], k4cv.h, -0.2, 0.2, k4cv.k, 0.2, -0.2, 10))" ] } ], "metadata": { "kernelspec": { "display_name": "hklpy2", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.2" } }, "nbformat": 4, "nbformat_minor": 4 }