{
"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",
""
]
},
{
"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
}