{ "cells": [ { "cell_type": "markdown", "id": "cab9a69a", "metadata": {}, "source": [ "# Virtual diffractometer axis\n", "\n", "Demonstrate a diffractometer where one of the expected axes must be computed\n", "from one or more additional diffractometer positioners.\n", "\n", "One case might be where a rotational axis $a$ is operated by a tangent linear\n", "translation $y$ at fixed distance $x$ from the center of rotation. In this\n", "simple sketch, the relationships are defined: {math}`\\tan{a} = y / x`\n", "\n", "![sketch](../_static/th_tth-virt-axis-sketch.png)" ] }, { "cell_type": "markdown", "id": "20185a38", "metadata": {}, "source": [ "## Preparation\n", "\n", "The virtual axis must be defined with the features common to the other\n", "diffractometer axes (all based on `ophyd.PositionerBase`).\n", "\n", "Since this is a virtual *real* axis and not a *pseudo*, the easiest way to\n", "ensure these features are available is to create a subclass of\n", "`ophyd.SoftPositioner` (based on `ophyd.PositionerBase`) and override the parts\n", "which are custom to the virtual axis. A virtual *pseudo* axis should subclass\n", "from `hklpy2.Hklpy2PseudoAxis`.\n", "\n", "The `ophyd.DerivedSignal` is not based on `ophyd.PositionerBase` and, as such,\n", "is missing many features of the positioner interface." ] }, { "cell_type": "markdown", "id": "520b6fe3", "metadata": {}, "source": [ "**Parameters**\n", "\n", "- $x$ : A constant specified when creating the diffractometer object. Same\n", " engineering units as $y$.\n", "- $y$ : The physical translation axis. Specify the *name* of this diffractometer\n", " Component when creating the diffractometer object. The virtual positioner\n", " class will access the named diffractometer Component.\n", "- $a$ : The virtual rotation angle. When $a$ is read, its position is computed\n", " from $y$. Any movement of $a$ is translated into a movement of $y$." ] }, { "cell_type": "markdown", "id": "13ebb885", "metadata": {}, "source": [ "### Virtual positioner class\n", "\n", "Using {class}`hklpy2.misc.VirtualPositionerBase` as a base class, create the\n", "custom class for the specifications of `tth`, our virtual rotation axis. Here, we override these methods:\n", "\n", "- `__init__()` : $x$ - Add additional `distance` keyword argument.\n", "- `forward()` : Compute $a$ from $y$.\n", "- `inverse()` : Compute $y$ from $a$." ] }, { "cell_type": "code", "execution_count": 1, "id": "78f766db", "metadata": {}, "outputs": [], "source": [ "import math\n", "from hklpy2.misc import VirtualPositionerBase\n", "\n", "\n", "class VirtualRotationPositioner(VirtualPositionerBase):\n", " \"\"\"Compute virtual rotation from physical translation axis.\"\"\"\n", "\n", " def __init__(self, *, distance: float = 100, **kwargs):\n", " \"\"\"Distance from translation axis zero position to center of rotation.\"\"\"\n", " self.distance = distance # same units as physical\n", " super().__init__(**kwargs)\n", "\n", " def forward(self, translation: float) -> float:\n", " \"\"\"Return virtual rotation angle (degrees) from physical translation.\"\"\"\n", " return math.atan2(translation, self.distance) * 180 / math.pi\n", "\n", " def inverse(self, rotation: float) -> float:\n", " \"\"\"Return physical translation from virtual rotation angle (degrees).\"\"\"\n", " return self.distance * math.tan(rotation * math.pi / 180)\n" ] }, { "cell_type": "markdown", "id": "185ca49a", "metadata": {}, "source": [ "### Custom diffractometer class\n", "\n", "Here, we pick the\n", "[4-circle](https://blueskyproject.io/hklpy2/diffractometers.html#solver-hkl-soleil-geometry-e4cv)\n", "`E4CV` geometry to demonstrate a diffractometer that uses this new\n", "{class}`VirtualRotationPositioner` class.\n", "\n", "- `tth` (as $a$): Override the existing `tth` Component with the\n", " `VirtualRotationPositioner`, supplying the additional kwargs.\n", "\n", "- `dy` (as $y$): Add a `dy` Component as a `SoftPositioner`, supplying initial\n", " position and limits.\n", "\n", "- `distance` (as $x$): Pass this constant as a keyword argument when constructing\n", " the diffractometer object.\n", "\n", "Note the `kind=\"hinted\"` kwarg, which designates a Component to be included in a\n", "live table or plot during a scan.\n", "\n", "
\n", "Why not use hklpy2.creator()?\n", "\n", "We must write our own Python class. The `hklpy2.creator()` is not prepared\n", "[yet](https://github.com/bluesky/hklpy2/issues/113) to create a diffractometer\n", "with a custom class and keyword arguments such as this one. \n", "\n", "
\n", "Maybe, in the future ...\n", "\n", "```python\n", "gonio = hklpy2.creator(\n", " name=\"gonio\",\n", " solver=\"hkl_soleil\",\n", " geometry=\"E4CV\",\n", " reals = {\n", " omega: None,\n", " chi: None,\n", " phi: None,\n", " tth: {\n", " \"class\": \"VirtualRotationPositioner\",\n", " \"init_pos\": 0,\n", " \"physical_name\": \"dy\",\n", " \"distance\": 1000,\n", " \"kind\": \"hinted\",\n", " },\n", " dy: {\n", " \"limits\": (-10, 200),\n", " # all other kwargs use defaults\n", " }\n", " }\n", ")\n", "```\n", "\n", "But we still need to add some code in the `__init__()` method.\n", "\n", "
\n", "\n", "
\n", "
\n", "\n", "Using the factory function to create a base class, we define a custom class:" ] }, { "cell_type": "code", "execution_count": 2, "id": "df837473", "metadata": {}, "outputs": [], "source": [ "import hklpy2\n", "from ophyd import Component, SoftPositioner\n", "\n", "MyBase = hklpy2.diffractometer_class_factory(\n", " solver=\"hkl_soleil\",\n", " geometry=\"E4CV\",\n", ")\n", "\n", "\n", "class MyGoniometer(MyBase):\n", " # Replace existing axis with virtual 2Theta.\n", " # Point it at the 'dy' axis (below).\n", " tth = Component(\n", " VirtualRotationPositioner,\n", " init_pos=0,\n", " physical_name=\"dy\",\n", " distance=1000,\n", " kind=\"hinted\",\n", " )\n", "\n", " # Add the translation axis 'dy'.\n", " dy = Component(\n", " SoftPositioner,\n", " init_pos=0,\n", " limits=(-10, 200),\n", " kind=\"hinted\",\n", " )\n", " \n", " def __init__(self, *args, **kwargs):\n", " super().__init__(*args, **kwargs)\n", " self.tth._finish_setup()" ] }, { "cell_type": "markdown", "id": "c4f1b034", "metadata": {}, "source": [ "### Diffractometer object\n", "\n", "- Move `dy` (physical axis), report `tth` and `l` at each step.\n", "- Move `tth` (virtual axis), report `dy` and `l` at each step.\n", "- Move `l` (pseudo axis), report `tth` and `dy` at each step." ] }, { "cell_type": "code", "execution_count": 3, "id": "1ed8c404", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "wavelength=1.0\n", "pseudos: h=0, k=0, l=0\n", "reals: omega=0, chi=0, phi=0, tth=0\n", "auxiliaries: dy=0\n" ] } ], "source": [ "gonio = MyGoniometer(name=\"gonio\")\n", "gonio.add_sample(\"vibranium\", 2*math.pi)\n", "gonio.wh()" ] }, { "cell_type": "markdown", "id": "3bac1de4", "metadata": {}, "source": [ "## Bluesky scans\n", "\n", "Demonstrate the relationships between `dy`, `tth`, and `l` through bluesky scans.\n", "\n", "- Scan `dy` (physical axis), report `tth` and `l` at each step.\n", "- Scan `tth` (virtual axis), report `dy` and `l` at each step.\n", "- Scan `l` (pseudo axis), report `tth` and `dy` at each step.\n", "\n", "Start with a simple setup, but no detectors or data collection. Just tables and plotting." ] }, { "cell_type": "code", "execution_count": 4, "id": "8120c79c", "metadata": {}, "outputs": [], "source": [ "import bluesky\n", "from bluesky import plans as bp\n", "from bluesky.callbacks.best_effort import BestEffortCallback\n", "\n", "bec = BestEffortCallback()\n", "RE = bluesky.RunEngine()\n", "RE.subscribe(bec)\n", "bec.disable_plots()" ] }, { "cell_type": "markdown", "id": "4630575d", "metadata": {}, "source": [ "### Scan *physical* axis `dy`" ] }, { "cell_type": "code", "execution_count": 5, "id": "74c0eb0e", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Transient Scan ID: 1 Time: 2025-07-21 15:06:10\n", "Persistent Unique Scan ID: '288c4df3-b7e6-4439-a653-e051e6d3b2b6'\n", "New stream: 'primary'\n", "+-----------+------------+------------+------------+------------+\n", "| seq_num | time | gonio_dy | gonio_l | gonio_tth |\n", "+-----------+------------+------------+------------+------------+\n", "| 1 | 15:06:10.3 | -1.000 | -0.006 | -0.057 |\n", "| 2 | 15:06:10.3 | 0.100 | 0.001 | 0.006 |\n", "| 3 | 15:06:10.3 | 1.200 | 0.008 | 0.069 |\n", "| 4 | 15:06:10.3 | 2.300 | 0.014 | 0.132 |\n", "| 5 | 15:06:10.3 | 3.400 | 0.021 | 0.195 |\n", "| 6 | 15:06:10.3 | 4.500 | 0.028 | 0.258 |\n", "| 7 | 15:06:10.3 | 5.600 | 0.035 | 0.321 |\n", "| 8 | 15:06:10.3 | 6.700 | 0.042 | 0.384 |\n", "| 9 | 15:06:10.3 | 7.800 | 0.049 | 0.447 |\n", "| 10 | 15:06:10.3 | 8.900 | 0.056 | 0.510 |\n", "| 11 | 15:06:10.3 | 10.000 | 0.063 | 0.573 |\n", "+-----------+------------+------------+------------+------------+\n", "generator scan ['288c4df3'] (scan num: 1)\n", "\n", "\n", "\n", "wavelength=1.0\n", "pseudos: h=-0.0003, k=0, l=0.0628\n", "reals: omega=0, chi=0, phi=0, tth=0.5729\n", "auxiliaries: dy=10.0\n" ] } ], "source": [ "RE(bp.scan([gonio.dy, gonio.tth, gonio.l], gonio.dy, -1, 10, 11))\n", "gonio.wh()" ] }, { "cell_type": "markdown", "id": "21d87365", "metadata": {}, "source": [ "### Scan *virtual* axis `tth`" ] }, { "cell_type": "code", "execution_count": 6, "id": "e169dd81", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Transient Scan ID: 2 Time: 2025-07-21 15:06:10\n", "Persistent Unique Scan ID: 'defaaece-f675-4fb0-bce6-44845b0d0135'\n", "New stream: 'primary'\n", "+-----------+------------+------------+------------+------------+\n", "| seq_num | time | gonio_tth | gonio_dy | gonio_l |\n", "+-----------+------------+------------+------------+------------+\n", "| 1 | 15:06:10.5 | -0.100 | -1.745 | -0.011 |\n", "| 2 | 15:06:10.5 | -0.040 | -0.698 | -0.004 |\n", "| 3 | 15:06:10.5 | 0.020 | 0.349 | 0.002 |\n", "| 4 | 15:06:10.5 | 0.080 | 1.396 | 0.009 |\n", "| 5 | 15:06:10.5 | 0.140 | 2.443 | 0.015 |\n", "| 6 | 15:06:10.5 | 0.200 | 3.491 | 0.022 |\n", "| 7 | 15:06:10.5 | 0.260 | 4.538 | 0.029 |\n", "| 8 | 15:06:10.5 | 0.320 | 5.585 | 0.035 |\n", "| 9 | 15:06:10.5 | 0.380 | 6.632 | 0.042 |\n", "| 10 | 15:06:10.5 | 0.440 | 7.680 | 0.048 |\n", "| 11 | 15:06:10.5 | 0.500 | 8.727 | 0.055 |\n", "+-----------+------------+------------+------------+------------+\n", "generator scan ['defaaece'] (scan num: 2)\n", "\n", "\n", "\n", "wavelength=1.0\n", "pseudos: h=-0.0002, k=0, l=0.0548\n", "reals: omega=0, chi=0, phi=0, tth=0.5\n", "auxiliaries: dy=8.7269\n" ] } ], "source": [ "RE(bp.scan([gonio.dy, gonio.tth, gonio.l], gonio.tth, -0.1, 0.5, 11))\n", "gonio.wh()" ] }, { "cell_type": "markdown", "id": "894a9b24", "metadata": {}, "source": [ "### Scan *pseudo* axis `l`" ] }, { "cell_type": "code", "execution_count": 7, "id": "5054de14", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\n", "\n", "Transient Scan ID: 3 Time: 2025-07-21 15:06:10\n", "Persistent Unique Scan ID: '029043a7-f32d-4232-97a1-49029e9e3e2d'\n", "New stream: 'primary'\n", "+-----------+------------+------------+------------+------------+\n", "| seq_num | time | gonio_l | gonio_dy | gonio_tth |\n", "+-----------+------------+------------+------------+------------+\n", "| 1 | 15:06:10.7 | 0.010 | 1.592 | 0.091 |\n", "| 2 | 15:06:10.7 | 0.014 | 2.228 | 0.128 |\n", "| 3 | 15:06:10.7 | 0.018 | 2.865 | 0.164 |\n", "| 4 | 15:06:10.7 | 0.022 | 3.502 | 0.201 |\n", "| 5 | 15:06:10.7 | 0.026 | 4.138 | 0.237 |\n", "| 6 | 15:06:10.7 | 0.030 | 4.775 | 0.274 |\n", "| 7 | 15:06:10.7 | 0.034 | 5.411 | 0.310 |\n", "| 8 | 15:06:10.7 | 0.038 | 6.048 | 0.347 |\n", "| 9 | 15:06:10.7 | 0.042 | 6.685 | 0.383 |\n", "| 10 | 15:06:10.7 | 0.046 | 7.321 | 0.419 |\n", "| 11 | 15:06:10.7 | 0.050 | 7.958 | 0.456 |\n", "+-----------+------------+------------+------------+------------+\n", "generator scan ['029043a7'] (scan num: 3)\n", "\n", "\n", "\n", "wavelength=1.0\n", "pseudos: h=-0.0002, k=0, l=0.05\n", "reals: omega=0.228, chi=0, phi=-0.2741, tth=0.456\n", "auxiliaries: dy=7.958\n" ] } ], "source": [ "RE(bp.scan([gonio.dy, gonio.tth, gonio.l], gonio.l, 0.01, 0.05, 11))\n", "gonio.wh()" ] } ], "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.12.11" } }, "nbformat": 4, "nbformat_minor": 5 }