NSLS-II TARDIS Configuration#

This notebook documents some calculations using a simulation of the Tardis diffractometer (an E6C variation) at NSLS-II. The connections with the EPICS motors have been replaced with simulated motors for this example.

Using the E6C geometry from libhkl, @cmazzoli found that, in this geometry, with the “lifting_detector_mu” mode, the following mapping applies:

libhkl

TARDIS

mu

theta

gamma

delta

delta

gamma

phi

None

chi

None

omega

None

  • The diffractometer geometry with angle and axis definitions are depicted below

20d367b739a84fac97d0066af29b271e

Begin by instantiating a calculation engine of the appropriate geometry, and configuring its mode as lifting_detector_mu . We must reach into the hkl.calc module to import CalcE6C.

[1]:
from hkl.calc import CalcE6C

tardis_calc = CalcE6C()

# what modes are available?
print(f"Available modes: {tardis_calc.engine.modes = }")
print(f"{tardis_calc.physical_axes = }")
print(f"{tardis_calc.pseudo_axes = }")
Available modes: tardis_calc.engine.modes = ['bissector_vertical', 'constant_omega_vertical', 'constant_chi_vertical', 'constant_phi_vertical', 'lifting_detector_phi', 'lifting_detector_omega', 'lifting_detector_mu', 'double_diffraction_vertical', 'bissector_horizontal', 'double_diffraction_horizontal', 'psi_constant_vertical', 'psi_constant_horizontal', 'constant_mu_horizontal']
tardis_calc.physical_axes = OrderedDict([('mu', 0.0), ('omega', 0.0), ('chi', 0.0), ('phi', 0.0), ('gamma', 0.0), ('delta', 0.0)])
tardis_calc.pseudo_axes = OrderedDict([('h', 0.0), ('k', 0.0), ('l', 0.0)])
[2]:
tardis_calc.engine.mode = 'lifting_detector_mu'

Next, seed the calculation engine with a parameterized sample and wavelength (or energy).

NOTE: length units are in Angstrom, angles are in degrees, and energy is in keV.

[3]:
from hkl import Lattice

# lattice cell lengths are in Angstrom, angles are in degrees
lattice = Lattice(a=9.069, b=9.069, c=10.390, alpha=90.0, beta=90.0, gamma=120.0)
sample = tardis_calc.new_sample('sample1', lattice=lattice)

print(f"{sample = }")
sample = HklSample(name='sample1', lattice=LatticeTuple(a=9.069, b=9.069, c=10.39, alpha=90.0, beta=90.0, gamma=119.99999999999999), ux=Parameter(name='None (internally: ux)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree'), uy=Parameter(name='None (internally: uy)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree'), uz=Parameter(name='None (internally: uz)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree'), U=array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]]), UB=array([[ 7.99999720e-01,  3.99999860e-01, -6.41365809e-17],
       [ 0.00000000e+00,  6.92820080e-01, -6.41365809e-17],
       [ 0.00000000e+00,  0.00000000e+00,  6.04733908e-01]]), reflections=[])
[4]:
tardis_calc.wavelength = 1.61198  # in Angstrom

# just to check
print(f"Energy: {tardis_calc.energy = } keV")
Energy: tardis_calc.energy = 7.691422871251505 keV

Now, apply constraints appropriate for TARDIS’ geometry. This includes setting limits on the acceptable ranges of motion, initial (and constant!) values, and whether or not a particular axis should be factored into the fitting function that produces the forward and inverse solutions.

Since we are working with a hkl.calc.CalcRecip object, we do not have access to the convenience of the apply_constraints() method provided by the hkl.diffract.Diffractometer class. We have to set the constraints the hard way. These are the same steps implemented within apply_constraints().

NOTE: physical motors should be checked that limits are in place prior to initiating any motion. Note also that none of the calculations below are associated with any physical motors, and that there is no connection between “limit” values used in the calculation, and soft-limit values that may be present in a control system for physical motors.

[5]:
# Theta
mu = tardis_calc['mu']
mu.limits = (-181, 181)
mu.value = 0

# we don't have it. Fix to 0
phi = tardis_calc['phi']
phi.limits = (0, 0)
phi.value = 0

# we don't have it. Fix to 0
chi = tardis_calc['chi']
chi.limits = (0, 0)
chi.value = 0

# we don't have it!! Fix to 0
omega = tardis_calc['omega']
omega.limits = (0, 0)
omega.value = 0

# NOTE: Tardis detector stage names are swapped from canonical names!
# delta
gamma = tardis_calc['gamma']
gamma.limits = (-5, 180)
gamma.value = 0

# gamma
delta = tardis_calc['delta']
delta.limits = (-5, 180)
delta.value = 0

We can take a look at the UB matrix, but thus far, it won’t be very interesting

[6]:
print(f"{tardis_calc.sample.UB = }")
tardis_calc.sample.UB = array([[ 7.99999720e-01,  3.99999860e-01, -6.41365809e-17],
       [ 0.00000000e+00,  6.92820080e-01, -6.41365809e-17],
       [ 0.00000000e+00,  0.00000000e+00,  6.04733908e-01]])

Add two, known reflections and the motor positions associated with those hkl values. Here, we are using values from @cmazolli’s ESRF notes:

(3,3,0): del = 64.449, gam = -0.871, th = 25.285
(5,2,0): del = 79.712, gam = -1.374, th = 46.816

NOTE: the translation of gamma==delta, delta==gamma, and mu==theta is being used

[7]:
r1 = tardis_calc.sample.add_reflection(3, 3, 0,
                           position=tardis_calc.Position(gamma=64.449, mu=25.285, chi=0.0, phi=0.0, omega=0.0, delta=-0.871))

r2 = tardis_calc.sample.add_reflection(5, 2, 0,
                           position=tardis_calc.Position(gamma=79.712, mu=46.816, chi=0.0, phi=0.0, omega=0.0, delta=-1.374))
[8]:
print(f"{tardis_calc.sample.reflections = }")
tardis_calc.sample.reflections = [(h=3.0, k=3.0, l=0.0), (h=5.0, k=2.0, l=0.0)]

Now a UB matrix can be computed.

[9]:
tardis_calc.sample.compute_UB(r1, r2)
[9]:
array([[ 0.31323551, -0.4807593 ,  0.01113654],
       [ 0.73590724,  0.63942704,  0.01003773],
       [-0.01798898, -0.00176066,  0.60454803]])

Compare some libhkl-generated results with those from @cmazolli’s notes:

# Experimentally found reflections @ Lambda = 1.61198 A
# (4, 4, 0) = [90.628, 38.373, 0, 0, 0, -1.156]
# (4, 1, 0) = [56.100, 40.220, 0, 0, 0, -1.091]
# @ Lambda = 1.60911
# (6, 0, 0) = [75.900, 61.000, 0, 0, 0, -1.637]
# @ Lambda = 1.60954
# (3, 2, 0) = [53.090, 26.144, 0, 0, 0, -.933]
# (5, 4, 0) = [106.415, 49.900, 0, 0, 0, -1.535]
# (4, 5, 0) = [106.403, 42.586, 0, 0, 0, -1.183]
[10]:
print(f"{tardis_calc.forward((4,4,0)) = }")
tardis_calc.forward((4,4,0)) = (PosCalcE6C(mu=38.37622128052063, omega=0.0, chi=0.0, phi=0.0, gamma=90.63030469353308, delta=-1.1613181970939916),)
[11]:
print(f"{tardis_calc.forward((4,1,0)) = }")
tardis_calc.forward((4,1,0)) = (PosCalcE6C(mu=40.219919777570965, omega=0.0, chi=0.0, phi=0.0, gamma=56.09704093977083, delta=-1.0836608655032929),)

Change wavelength here to 1.60911 Angstrom. Note the difference below in delta (TARDIS’ gamma axis)

[12]:
# change wavelength (Angstrom)
tardis_calc.wavelength = 1.60911
print(f"{tardis_calc.forward((6,0,0)) = }")
tardis_calc.forward((6,0,0)) = (PosCalcE6C(mu=60.99346591074179, omega=0.0, chi=0.0, phi=0.0, gamma=75.84521749189145, delta=-1.5839501607961701),)
[13]:
tardis_calc.wavelength = 1.60954
print(f"{tardis_calc.forward((3,2,0)) = }")
print(f"{tardis_calc.forward((5,4,0)) = }")
print(f"{tardis_calc.forward((4,5,0)) = }")
tardis_calc.forward((3,2,0)) = (PosCalcE6C(mu=26.173823521308144, omega=0.0, chi=0.0, phi=0.0, gamma=53.05207622287554, delta=-0.8437995840438257),)
tardis_calc.forward((5,4,0)) = (PosCalcE6C(mu=49.892322604056034, omega=0.0, chi=0.0, phi=0.0, gamma=106.32053081067252, delta=-1.423656049079967),)
tardis_calc.forward((4,5,0)) = (PosCalcE6C(mu=42.54926633295045, omega=0.0, chi=0.0, phi=0.0, gamma=106.31894239326303, delta=-1.1854071532601609),)

HKL PseudoPositioner Use#

Let’s explore the idea of an hkl ‘motor’

[14]:
from ophyd import Component as Cpt
from ophyd import (PseudoSingle, EpicsMotor)
from hkl import SimulatedE6C

# FIXME: hack to get around what should have been done at init of tardis_calc instance
tardis_calc._lock_engine = True

class Tardis(SimulatedE6C): ...

# class Tardis(E6C):
#     h = Cpt(PseudoSingle, '')
#     k = Cpt(PseudoSingle, '')
#     l = Cpt(PseudoSingle, '')

#     theta = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X1}Mtr')
#     omega = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X2}Mtr')
#     chi = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X3}Mtr')
#     phi = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X4}Mtr')
#     delta = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X5}Mtr')
#     gamma = Cpt(EpicsMotor, 'XF:31IDA-OP{Tbl-Ax:X6}Mtr')

# re-map Tardis' axis names onto what an E6C expects
name_map = {
    # tardis: E6C
    'mu': 'theta',
    'omega': 'omega',
    'chi': 'chi',
    'phi': 'phi',
    'gamma': 'delta',
    'delta': 'gamma',
    }

tardis = Tardis(
    '', # no prefix
    name='tardis', # local name
    calc_inst=tardis_calc, # the calc engine setup above
    # energy=tardis_calc.energy, # FIXME: unexpected keyword argument
)
tardis.calc.physical_axis_names = name_map
print(f"{tardis.calc.physical_axis_names = }")
tardis.calc.physical_axis_names = ['theta', 'omega', 'chi', 'phi', 'delta', 'gamma']
[15]:
print(f"{tardis.real_position = }")
tardis.real_position = TardisRealPos(mu=0, omega=0, chi=0, phi=0, gamma=0, delta=0)
[16]:
print(f"{tardis.connected = }")
tardis.connected = True
[17]:
print(f"Energy: {tardis.energy.get() = } keV")
Energy: tardis.energy.get() = 8.0 keV

Move to (101) reflection#

[18]:
tardis.move((1,0,1), wait=False)
[18]:
MoveStatus(done=False, pos=tardis, elapsed=0.0, success=False, settle_time=0.0)
[19]:
status = _
[20]:
print(f"{status.done = }")
status.done = True
[21]:
print(f"{tardis.real_position = }")
tardis.real_position = TardisRealPos(mu=32.61342481972243, omega=0.0, chi=0.0, phi=0.0, gamma=12.011255441335317, delta=8.64179902840924)
[22]:
print(f"{tardis.position = }")
tardis.position = TardisPseudoPos(h=1.0000000000000002, k=-5.667215834780817e-16, l=1.0)

Move to (102) reflection#

[23]:
tardis.move((1,0,2))
[23]:
MoveStatus(done=True, pos=tardis, elapsed=0.0, success=True, settle_time=0.0)
[24]:
tardis.h.describe()
[24]:
OrderedDict([('tardis_h',
              {'source': 'PY:tardis_h.position',
               'dtype': 'number',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_h_setpoint',
              {'source': 'PY:tardis_h.target',
               'dtype': 'integer',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''})])
[25]:
tardis.h.read()
[25]:
OrderedDict([('tardis_h', {'value': 1.0, 'timestamp': 1700539353.9169118}),
             ('tardis_h_setpoint',
              {'value': 1, 'timestamp': 1700539353.9169252})])
[26]:
tardis.describe()
[26]:
OrderedDict([('tardis_h',
              {'source': 'PY:tardis_h.position',
               'dtype': 'number',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_h_setpoint',
              {'source': 'PY:tardis_h.target',
               'dtype': 'integer',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_k',
              {'source': 'PY:tardis_k.position',
               'dtype': 'number',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_k_setpoint',
              {'source': 'PY:tardis_k.target',
               'dtype': 'integer',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_l',
              {'source': 'PY:tardis_l.position',
               'dtype': 'number',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_l_setpoint',
              {'source': 'PY:tardis_l.target',
               'dtype': 'integer',
               'shape': [],
               'upper_ctrl_limit': 0,
               'lower_ctrl_limit': 0,
               'units': ''}),
             ('tardis_mu',
              {'source': 'computed',
               'dtype': 'number',
               'shape': [],
               'units': '',
               'lower_ctrl_limit': -180,
               'upper_ctrl_limit': 180}),
             ('tardis_omega',
              {'source': 'computed',
               'dtype': 'number',
               'shape': [],
               'units': '',
               'lower_ctrl_limit': -180,
               'upper_ctrl_limit': 180}),
             ('tardis_chi',
              {'source': 'computed',
               'dtype': 'number',
               'shape': [],
               'units': '',
               'lower_ctrl_limit': -180,
               'upper_ctrl_limit': 180}),
             ('tardis_phi',
              {'source': 'computed',
               'dtype': 'number',
               'shape': [],
               'units': '',
               'lower_ctrl_limit': -180,
               'upper_ctrl_limit': 180}),
             ('tardis_gamma',
              {'source': 'computed',
               'dtype': 'number',
               'shape': [],
               'units': '',
               'lower_ctrl_limit': -180,
               'upper_ctrl_limit': 180}),
             ('tardis_delta',
              {'source': 'computed',
               'dtype': 'number',
               'shape': [],
               'units': '',
               'lower_ctrl_limit': -180,
               'upper_ctrl_limit': 180})])
[27]:
tardis.read()
[27]:
OrderedDict([('tardis_h', {'value': 1.0, 'timestamp': 1700539353.9169118}),
             ('tardis_h_setpoint',
              {'value': 1, 'timestamp': 1700539353.9169252}),
             ('tardis_k',
              {'value': -1.0351079150284598e-17,
               'timestamp': 1700539353.9170356}),
             ('tardis_k_setpoint',
              {'value': 0, 'timestamp': 1700539353.917049}),
             ('tardis_l',
              {'value': 1.9999999999999998, 'timestamp': 1700539353.9170997}),
             ('tardis_l_setpoint',
              {'value': 2, 'timestamp': 1700539353.9171119}),
             ('tardis_mu',
              {'value': 42.936333275662705, 'timestamp': 1700539353.9886892}),
             ('tardis_omega', {'value': 0.0, 'timestamp': 1700539353.9886923}),
             ('tardis_chi', {'value': 0.0, 'timestamp': 1700539353.9886947}),
             ('tardis_phi', {'value': 0.0, 'timestamp': 1700539353.9886968}),
             ('tardis_gamma',
              {'value': 12.143148125839154, 'timestamp': 1700539353.9886992}),
             ('tardis_delta',
              {'value': 17.765469685728892, 'timestamp': 1700539353.9887023})])
[28]:
print(f"{tardis.position = }")
tardis.position = TardisPseudoPos(h=1.0, k=-1.0351079150284598e-17, l=1.9999999999999998)
[29]:
print(f"{tardis.real_position = }")
tardis.real_position = TardisRealPos(mu=42.936333275662705, omega=0.0, chi=0.0, phi=0.0, gamma=12.143148125839154, delta=17.765469685728892)