How to Work with a Diffractometer#
See also
Diffractometer — conceptual overview of the diffractometer.
Tutorial: Your First Diffractometer — step-by-step first experience: create, orient, and scan.
Steps to Define a Diffractometer Object#
Identify the geometry.
Find its Solver, geometry, and other parameters in Diffractometers.
Create a custom subclass for the diffractometer.
(optional) Identify the EPICS PVs for the real positioners.
(optional) Connect wavelength to the control system.
Define the diffractometer object using
hklpy2.creator().
A Diffractometer Object#
A DiffractometerBase instance has several key
attributes. Brief descriptions follow; see the linked concept pages for details.
Attribute |
Description |
|---|---|
|
User-chosen label. By convention, match the Python variable name:
|
|
Physical arrangement of real positioners, pseudo axes, and extra parameters. Choices are limited to those provided by the chosen Solver. See Diffractometers. |
|
All operations are coordinated through Core Operations.
Accessible as |
|
Incident radiation described by a
|
|
The crystal sample, including its lattice and orientation reflections. See Sample. |
|
Controls which solution |
Note
The wavelength, commonly written as \(\lambda\), cannot be named in
Python code as lambda — Python reserves that keyword.
Here is an example set of orientation reflections for crystalline Vibranium [1] on an E4CV diffractometer:
# |
h |
k |
l |
omega |
chi |
phi |
tth |
wavelength |
order |
|---|---|---|---|---|---|---|---|---|---|
1 |
4 |
0 |
0 |
-145.451 |
0.0 |
0.0 |
69.0966 |
1.54 |
– |
2 |
0 |
4 |
0 |
-145.451 |
0.0 |
90.0 |
69.0966 |
1.54 |
1 |
3 |
0 |
0 |
4 |
-145.451 |
90.0 |
0.0 |
69.0966 |
1.54 |
2 |
The order column shows each reflection’s position in
fourc.sample.reflections.order (-- means not selected).
The reflections labeled 1 and 2 in that column are the first and second
orienting reflections passed to calc_UB()
to compute the UB matrix. Reflection 1 is stored but not selected
— it can serve as a verification point or be promoted later via
set_orientation_reflections().
Use a Diffractometer with the bluesky RunEngine#
See also
Tutorial: Your First Diffractometer for the basic create-orient-move workflow.
The positioners of a DiffractometerBase object may be
used with the bluesky RunEngine
with any of the plans in bluesky.plans or in custom plans of your own.
Multiple motors may be scanned simultaneously (as in the 0kl and diagonal
examples below). Consult bluesky.plans for the full list of available
scan plans.
1from hklpy2.run_utils import ConfigurationRunWrapper
2
3fourc = hklpy2.creator(name="fourc")
4
5# Save configuration with every run
6crw = ConfigurationRunWrapper(fourc)
7RE.preprocessors.append(crw.wrapper)
8
9# steps not shown here:
10# define a sample & orientation reflections, and compute UB matrix
11
12# record the diffractometer metadata to a run
13RE(bp.count([fourc]))
14
15# relative (h00) scan — first move k and l to 0
16fourc.move(0, 0, 0)
17RE(bp.rel_scan([scaler, fourc], fourc.h, -0.1, 0.1, 21))
18
19# absolute (0kl) scan — first move h to 0
20fourc.move(0, 1, 1)
21RE(bp.scan([scaler, fourc], fourc.k, 0.9, 1.1, fourc.l, 2, 3, 21))
22
23# scan h, k, l together along a diagonal — all three vary, no pre-move needed
24RE(bp.scan([scaler, fourc], fourc.h, 0.9, 1.1, fourc.k, 0.9, 1.1, fourc.l, 0.9, 1.1, 21))
25
26# absolute chi scan (real axis)
27RE(bp.scan([scaler, fourc], fourc.chi, 30, 60, 31))
Keep in mind these considerations:
Use the
hklpy2.run_utils.ConfigurationRunWrapperto save configuration as part of every run:1from hklpy2.run_utils import ConfigurationRunWrapper 2crw = ConfigurationRunWrapper(fourc) 3RE.preprocessors.append(crw.wrapper)
Only restore orientation reflections from a matching diffractometer geometry (such as
E4CV). Mismatch will trigger an exception.Scans use either
forward()(pseudo axes → real positions) orinverse()(real positions → pseudo axes), but never both at once. This means you must scan only pseudo axes (h,k,l, …) or only real axes (omega,chi,phi,tth, …) in a single scan — never a mix of both types:1# Cannot mix pseudo and real axes in a single scan. 2# This will raise a `ValueError` exception. 3RE(bp.scan([scaler, fourc], fourc.k, 0.9, 1.1, fourc.chi, 2, 3, 21))
When scanning with pseudo axes (
h,k,l,q, …), first check that all steps in the scan can be computed successfully with theforward()computation:fourc.forward(1.9, 0, 0)
Diffractometer Axis Names#
In hklpy2, the names of diffractometer axes (pseudos and reals) are not required to match any particular Solver library. Users are free to use any names allowed by ophyd.
Every Solver geometry defines a fixed expected order for its pseudo and
real axes (for example, the E4CV geometry expects pseudos h, k, l and
reals omega, chi, phi, tth in that order). By default,
creator() maps user-defined axis names to solver
axis slots positionally — first user name to first solver slot, and so
on. This works automatically when the names are supplied in the solver’s
expected order.
When user-defined names are supplied in a different order than the
solver expects, or when extra axes are present, positional mapping
produces a silently incorrect axes_xref. Use the _pseudo and/or
_real keywords to declare the correct mapping explicitly. See
Advanced: Axes Supplied Out of Order for details and examples.
User-defined axis names#
Let’s see examples of diffractometers built with user-defined names.
Diffractometer Creator — automatic (positional) mapping
Custom Diffractometer with additional axes — directed mapping with
_real
Diffractometer Creator#
The creator() function constructs a diffractometer
object using the supplied reals={} to define their names. These are mapped to
the names used by the Solver. Let’s show this cross-reference map
(twoc.core.axes_xref, in this case) with just a few commands:
>>> import hklpy2
>>> twoc = hklpy2.creator(
name="twoc",
geometry="TH TTH Q",
solver="th_tth",
reals={"sample": None, "detector": None},
)
>>> twoc.core.axes_xref
{'q': 'q', 'sample': 'th', 'detector': 'tth'}
>>> twoc.wh()
q=0
wavelength=1.0
sample=0, detector=0
Custom Diffractometer class#
Construct a 2-circle diffractometer, one axis for the sample and one axis for the detector.
In addition to defining the diffractometer axes, we name the Solver to use
with our diffractometer. The th_tth Solver has a
ThTthSolver with a "TH TTH Q" geometry
that fits our design. We set that up in the __init__() method of our new
class.
The TH TTH Q geometry has real axes named
th and tth. Even though we are using different names, it is not
necessary to define _real (as shown in Custom Diffractometer with additional axes)
as long as:
We define the same number of pseudos as the solver expects.
We define the same number of reals as the solver expects.
We specify each in the order expected by the solver.
1import hklpy2
2from hklpy2.diffract import Hklpy2PseudoAxis
3from ophyd import Component, SoftPositioner
4
5class S1D1(hklpy2.DiffractometerBase):
6
7 q = Component(Hklpy2PseudoAxis, "", kind=H_OR_N)
8
9 sample = Component(SoftPositioner, init_pos=0)
10 detector = Component(SoftPositioner, init_pos=0)
11
12 # Alias 'sample' to 'th', 'detector' to 'tth'
13 _real = ["sample", "detector"]
14
15 def __init__(self, *args, **kwargs):
16 super().__init__(
17 *args,
18 solver="th_tth", # solver name
19 geometry="TH TTH Q", # solver geometry
20 **kwargs,
21 )
Create a Python object that uses this class:
twoc = S1D1(name="twoc")
Tip
Use the hklpy2.diffract.creator() instead:
twoc = hklpy2.creator(
name="twoc",
geometry="TH TTH Q",
solver="th_tth",
reals=dict(sample=None, detector=None)
)
Show the mapping between user-defined axes and axis names used by the Solver:
>>> print(twoc.core.axes_xref)
{'q': 'q', 'sample': 'th', 'detector': 'tth'}
Custom Diffractometer with additional axes#
Consider this example for a two-circle class (with additional axes).
The "TH TTH Q" Solver geometry expects q as
the only pseudo axis and th and tth as the two real axes
(no extra axes).
We construct this example so that we’ll need to override the automatic
assignment of axes (lots of extra pseudo and real axes, none of them in the
order expected by the solver). Look for the _pseudo=["q"] and
_real=["theta", "ttheta"] parts where we define the mapping.
1import hklpy2
2from hklpy2.diffract import Hklpy2PseudoAxis
3from ophyd import Component, SoftPositioner
4
5class MyTwoC(hklpy2.DiffractometerBase):
6
7 # sorted alphabetically for this example
8 another = Component(Hklpy2PseudoAxis)
9 horizontal = Component(SoftPositioner, init_pos=0)
10 q = Component(Hklpy2PseudoAxis)
11 theta = Component(SoftPositioner, init_pos=0)
12 ttheta = Component(SoftPositioner, init_pos=0)
13 vertical = Component(SoftPositioner, init_pos=0)
14
15 _pseudo = ["q"]
16 _real = ["theta", "ttheta"]
17
18 def __init__(self, *args, **kwargs):
19 super().__init__(
20 *args,
21 solver="th_tth",
22 geometry="TH TTH Q",
23 **kwargs
24 )
Create the diffractometer:
twoc = MyTwoC(name="twoc")
What are the axes names used by this diffractometer?
>>> twoc.pseudo_axis_names
['another', 'q']
>>> twoc.real_axis_names
['horizontal', 'theta', 'ttheta', 'vertical']
Show the twoc diffractometer’s Solver:
>>> twoc.core.solver
ThTthSolver(name='th_tth', version='0.0.14', geometry='TH TTH Q')
What are the axes expected by this Solver?
>>> twoc.core.solver_pseudo_axis_names
['q']
>>> twoc.core.solver_real_axis_names
['th', 'tth']
>>> twoc.core.solver_extra_axis_names
[]
Show the cross-reference mapping from diffractometer to Solver axis names (as defined in our MyTwoC class above):
>>> twoc.core.axes_xref
{'q': 'q', 'theta': 'th', 'ttheta': 'tth'}
Advanced: Axes Supplied Out of Order#
When custom axis names are defined in a different order than the Solver
expects — or when extra axes are present — use the _pseudo and _real
keywords to declare the correct mapping explicitly. Without them, axes are
zipped positionally and can be silently swapped, causing incorrect
axes_xref entries and potentially degenerate UB matrices.
Pseudos supplied in a different order than the solver expects#
The _pseudo keyword has two related uses:
Pseudos out of order — custom names defined in a different order than the Solver expects.
Additional pseudos — extra pseudo axes are present and only a subset should be mapped to the Solver.
Without _pseudo, creator() zips pseudo names
positionally against the solver’s pseudo axis order. If orders differ, or
if extra pseudos are present, the mapping will be incorrect.
Use 1: pseudos out of order
An E4CV diffractometer with custom pseudo names supplied in a different order
than the solver’s h, k, l:
Without _pseudo, names are zipped positionally: ll (1st)
maps to solver h (1st) and hh (3rd) maps to solver l
(3rd) — silently swapped:
sim = hklpy2.creator(
name="sim",
solver="hkl_soleil",
geometry="E4CV",
pseudos=["ll", "kk", "hh"],
)
sim.core.axes_xref
# {'ll': 'h', 'kk': 'k', 'hh': 'l', ...} ← swapped
Supply _pseudo to declare which local name maps to each solver
pseudo axis slot, independent of the pseudos list order:
sim = hklpy2.creator(
name="sim",
solver="hkl_soleil",
geometry="E4CV",
pseudos=["ll", "kk", "hh"],
_pseudo=["hh", "kk", "ll"],
)
sim.core.axes_xref
# {'hh': 'h', 'kk': 'k', 'll': 'l', ...} ← correct
Use 2: additional pseudos
When extra pseudo axes are added alongside the solver’s pseudos, _pseudo
selects exactly which names map to the Solver. The remaining pseudos are
available as diffractometer attributes but are not included in the solver
mapping:
sim = hklpy2.creator(
name="sim",
solver="hkl_soleil",
geometry="E4CV",
pseudos=["hh", "kk", "ll", "extra"],
_pseudo=["hh", "kk", "ll"], # select these 3 for solver mapping
)
sim.core.axes_xref
# {'hh': 'h', 'kk': 'k', 'll': 'l', ...} ← 'extra' not mapped
sim.pseudo_axis_names
# ['hh', 'kk', 'll'] ← 'extra' excluded
Reals supplied in a different order than the solver expects#
When hardware motor names are wired or named in a different order than the
Solver expects, the _real keyword is required to declare which local
axis name corresponds to each Solver axis slot.
Without _real, creator() (and
diffractometer_class_factory()) zip the reals
dict keys positionally against the solver’s axis order. If these orders
differ, axes are silently swapped in axes_xref, which can cause
calc_UB() to fail with a degenerate U matrix.
Example: an APS POLAR 6-circle diffractometer whose hardware motors are
wired in a different order than the solver expects
(solver order: tau, mu, chi, phi, gamma, delta):
Without _real, reals dict keys are zipped positionally to
solver axes. gamma (3rd key) maps to solver chi (3rd slot)
and chi (5th key) maps to solver gamma (5th slot) — silently
swapped:
cradle = hklpy2.creator(
name="cradle",
solver="hkl_soleil",
geometry="APS POLAR",
reals=dict(tau="m73", mu="m4", gamma="m19", delta="m20", chi="m37", phi="m38"),
)
cradle.core.axes_xref
# {'tau': 'tau', 'mu': 'mu', 'gamma': 'chi', 'delta': 'phi',
# 'chi': 'gamma', 'phi': 'delta'} ← swapped
Supply _real to declare which local name maps to each solver axis
slot, independent of the reals dict key order:
cradle = hklpy2.creator(
name="cradle",
solver="hkl_soleil",
geometry="APS POLAR",
reals=dict(tau="m73", mu="m4", gamma="m19", delta="m20", chi="m37", phi="m38"),
_real="tau mu chi phi gamma delta".split(),
)
cradle.core.axes_xref
# {'tau': 'tau', 'mu': 'mu', 'chi': 'chi', 'phi': 'phi',
# 'gamma': 'gamma', 'delta': 'delta'} ← correct