Diffractometer Extra Motors and/or Pseudos#

Sometimes, it is desired to add additional ophyd components to a diffractometer object. Such components could include additional motor axes, azimuthal reference vectors, temperature, etc.

Objective

Add one or more real positioners to the standard positioners of the 2-circle diffractometer (tth_tth, TH TTH Q geometry). Use simulated motors for the example (no EPICS required).

Standard 2-circle#

First, we start with the setup of a 2-circle ($\theta:2\theta$) diffractometer.

axis

space

th

real

tth

real

q

pseudo

Create the diffractometer object.

import hklpy2

th2th = hklpy2.creator(name="th2th", geometry="TH TTH Q", solver="th_tth")

Show the diffractometer configuration and all the ophyd components.

th2th.wh()
print(f"{th2th.component_names=}")
q=0
wavelength=1.0
th=0, tth=0
th2th.component_names=('geometry', 'solver', 'wavelength', 'q', 'th', 'tth')

Add additional positioner#

We can use the hklpy2.creator() for the additional positioner. Since we are not using the default reals, we’ll provide a Python dictionary that defines each real axis, in order, and whether it uses a simulated motor or an EPICS PV. None means to use a simulated motor.

th2th = hklpy2.creator(
    name="th2th",
    geometry="TH TTH Q",
    solver="th_tth",
    reals=dict(th=None, tth=None, spinner=None)
)
th2th.wh()
print(f"{th2th.component_names=}")
q=0
wavelength=1.0
th=0, tth=0
th2th.component_names=('geometry', 'solver', 'wavelength', 'q', 'th', 'tth', 'spinner')

Compare these results. The new result adds the spinner axis.

Set (and show) the limits on the spinner:

th2th.spinner._limits = -10_000, 10_000
th2th.spinner.limits
(-10000, 10000)

Can we add other pseudo axes?#

Q: With this capability to add additional real positioners, can we add axes to the pseudo positioners?

A: Yes. See this example. It defines two pseudo axes: Q and d. As shown in aliases, the Q axis is mapped to the solver q axis.

th2th = hklpy2.creator(
    name="th2th",
    geometry="TH TTH Q",
    solver="th_tth",
    pseudos=["Q", "d"],  # 
    reals=dict(sample=None, detector=None, spinner=None),
    aliases=dict(pseudos=["Q"], reals=["sample", "detector"]),
)
th2th.wh()
Q=0
wavelength=1.0
sample=0, detector=0

Add additional Signals and Devices#

Finally, we add additional Signals and Component Devices as a demonstration.

The {func}function]~hklpy2.diffract.creator() has its limits. The creator() relies on a {func}function]~hklpy2.diffract.diffractometer_class_factory(). Let’s skip the factory function and show how to build a structure directly.

Demonstrate a variety of additional components.

from hklpy2.diffract import Hklpy2PseudoAxis
from ophyd import Component, Device, Signal, SoftPositioner
from ophyd.signal import SignalRO


class XYStage(Device):
    x = Component(SoftPositioner, kind="hinted", limits=(-20, 105), init_pos=0)
    y = Component(SoftPositioner, kind="hinted", limits=(-20, 105), init_pos=0)
    solenoid_lock = Component(Signal, value=True, kind="normal")


class MyTwoCircle(hklpy2.DiffractometerBase):
    _real = ["th", "tth"]

    q = Component(Hklpy2PseudoAxis, "", kind="hinted")
    th = Component(
        SoftPositioner, kind="hinted", limits=(-180, 180), egu="degrees", init_pos=0
    )
    tth = Component(
        SoftPositioner, kind="hinted", limits=(-180, 180), egu="degrees", init_pos=0
    )
    spinner = Component(
        SoftPositioner,
        kind="hinted",
        limits=(-10000, 10000),
        egu="rotations",
        init_pos=0,
    )
    atth = Component(
        SoftPositioner, kind="hinted", limits=(-180, 180), egu="degrees", init_pos=0
    )
    temperature = Component(SignalRO, value=25, kind="normal")
    xy = Component(XYStage, kind="normal")

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args,
            solver="th_tth",  # solver name
            geometry="TH TTH Q",  # solver geometry
            **kwargs,
        )


th2tth = MyTwoCircle(name="th2tth")
th2tth.wh()  # brief report of diffractometer position
# th2th.summary()  # show the full ophyd structure summary
th2tth.read()
q=0
wavelength=1.0
th=0, tth=0
OrderedDict([('th2tth_q', {'value': 0, 'timestamp': 1743011146.56252}),
             ('th2tth_q_setpoint',
              {'value': 0, 'timestamp': 1743011146.5625339}),
             ('th2tth_th', {'value': 0, 'timestamp': 1743011146.5971136}),
             ('th2tth_tth', {'value': 0, 'timestamp': 1743011146.5971181}),
             ('th2tth_spinner', {'value': 0, 'timestamp': 1743011146.5971215}),
             ('th2tth_atth', {'value': 0, 'timestamp': 1743011146.5971239}),
             ('th2tth_temperature',
              {'value': 25, 'timestamp': 1743011146.5629923}),
             ('th2tth_xy_x', {'value': 0, 'timestamp': 1743011146.5971382}),
             ('th2tth_xy_y', {'value': 0, 'timestamp': 1743011146.597141}),
             ('th2tth_xy_solenoid_lock',
              {'value': True, 'timestamp': 1743011146.563218})])