Tutorial: Your First Diffractometer#
By the end of this tutorial you will be able to:
create a simulated 4-circle diffractometer in Python
define a crystal sample and its lattice parameters
add two orientation reflections and compute the \(UB\) matrix
move to a reciprocal-space position
run a simple scan in reciprocal space
No hardware is required. Everything runs in a simulated environment. The same workflow applies to other diffractometer geometries — you simply substitute a different geometry name when creating the diffractometer.
Note
This tutorial uses the E4CV (4-circle vertical) geometry with the
hkl_soleil solver and silicon as the sample crystal. If you are
working with a different geometry or crystal, the steps are identical —
only the axis names, lattice parameters, and reflection angles change.
See also
Diffractometer — conceptual overview of the diffractometer.
Diffractometers — full table of available geometries and solvers.
Constraints — how constraints filter forward() solutions.
Prerequisites#
Install hklpy2 and its dependencies following the installation guide. Then start Python or a Jupyter notebook in an environment where hklpy2 is installed.
Step 1 — Create the diffractometer#
We use creator() to build a diffractometer object.
It handles all the wiring between the Python object, the solver, and
the simulated motor axes.
import hklpy2
fourc = hklpy2.creator(name="fourc", geometry="E4CV", solver="hkl_soleil")
The name keyword is a label for this diffractometer object — by
convention it matches the Python variable name. geometry="E4CV"
selects the 4-circle vertical geometry. solver="hkl_soleil" selects
the Hkl/Soleil backend library.
Tip
Run solvers() to see all installed solvers, and
inspect fourc.core.solver.geometries to see the geometries your
solver supports.
Step 2 — Import convenience functions and set the active diffractometer#
The hklpy2.user module provides interactive convenience functions
modelled on common SPEC commands. We import the ones we need and tell
hklpy2 which diffractometer is the active one:
from hklpy2.user import (
add_sample,
calc_UB,
cahkl,
cahkl_table,
pa,
set_diffractometer,
setor,
wh,
)
set_diffractometer(fourc)
All subsequent calls to pa(), wh(),
setor(), etc. will operate on fourc until you call
set_diffractometer() again with a different object.
Step 3 — Add a sample#
A sample pairs a name with a crystal lattice. hklpy2 includes the silicon lattice parameter as a built-in constant:
add_sample("silicon", a=hklpy2.SI_LATTICE_PARAMETER)
Silicon is cubic so only one lattice parameter a is needed.
Notice that add_sample() prints a confirmation:
Sample(name='silicon', lattice=Lattice(a=5.431, system='cubic'))
For a non-cubic crystal you would supply additional parameters, for
example a=3.0, c=5.0, gamma=120 for a hexagonal crystal.
Step 4 — Set the wavelength#
The wavelength of the incident X-rays is a property of the beam, not the crystal. We set it once and it applies to all subsequent calculations:
fourc.beam.wavelength.put(1.54) # Angstroms — Cu K-alpha
Notice we use .put() because
wavelength is an ophyd Signal.
At a real beamline this signal would be connected to the monochromator
control system, which may work in either wavelength or energy units —
WavelengthXray supports both.
Step 5 — Add orientation reflections#
The \(UB\) matrix encodes how the crystal is mounted on the diffractometer. To compute it we need at least two measured orientation reflections — positions where we know both the Miller indices \((h, k, l)\) and the motor angles.
We use setor() (“set orienting reflection”):
r1 = setor(4, 0, 0, tth=69.0966, omega=-145.451, chi=0, phi=0)
r2 = setor(0, 4, 0, tth=69.0966, omega=-145.451, chi=90, phi=0)
r1 is the \((4, 0, 0)\) reflection measured at those four motor
angles. r2 is the \((0, 4, 0)\) reflection.
Note
In a real experiment these angles come from your diffractometer control system — you physically drive to a known Bragg peak, read the motor positions, and record them here. In this tutorial the values are pre-calculated for silicon at Cu K-alpha.
Step 6 — Compute the UB matrix#
With two reflections recorded, we ask hklpy2 to compute the \(UB\) orientation matrix:
calc_UB(r1, r2)
The function returns and prints the \(3 \times 3\) matrix. The exact numbers depend on the geometry and crystal orientation — what matters is that the computation succeeded without error.
Now call pa() (“print all”) to see the full
diffractometer state:
pa()
You will see the solver, sample, reflections, \(UB\) matrix, constraints, mode, wavelength, and current position all in one place. Notice the \(U\) and \(UB\) matrices are now populated.
Step 7 — Verify the orientation#
Before moving any motors it is good practice to verify that
forward() and inverse() give consistent results with the
orientation reflections.
First, narrow the constraints so that only physically sensible solutions are returned — keeping \(2\theta\) positive and \(\omega\) negative:
fourc.core.constraints["tth"].limits = -0.001, 180
fourc.core.constraints["omega"].limits = (-180, 0.001)
Now check inverse() — given
the angles of the first reflection, do we recover \((4, 0, 0)\)?
fourc.inverse((-145.451, 0, 0, 69.0966))
# → Hklpy2DiffractometerPseudoPos(h=3.9999, k=0, l=0) ✓
And forward() — given
\((4, 0, 0)\), do we get back angles close to the measured
reflection?
fourc.core.mode = "bissector" # omega = tth / 2
fourc.forward(4, 0, 0)
# → Hklpy2DiffractometerRealPos(omega=-34.5491, chi=0.0, phi=-110.9011, tth=69.0982)
Note
The forward() answer may
differ from the measured reflection angles — both are valid positions
for \((4, 0, 0)\). There are often multiple geometrically
equivalent solutions. Use cahkl_table() to see
all of them:
cahkl_table((4, 0, 0), (0, 4, 0))
Each row is a valid solution. The constraints we set above have already filtered out solutions outside the motor ranges.
Step 8 — Move to a reciprocal-space position#
Once the orientation is verified, moving to an accessible \((h, k, l)\) position is straightforward:
Note
Not every position is reachable. Physical motor limits, the Ewald
sphere, the current constraints, and the wavelength all restrict which
reflections the diffractometer can reach. The wavelength sets the
radius of the Ewald sphere and therefore determines which
reciprocal-lattice points are in range at all; changing the wavelength
(or equivalently the energy) shifts that boundary. If forward()
returns no solutions, the position is inaccessible under the current
configuration.
fourc.move(4, 0, 0)
This drives all four motors simultaneously to the angles that correspond to \((4, 0, 0)\). Check the current position:
wh()
You will see the current \((h, k, l)\) pseudo position alongside the real motor angles — the two coordinate spaces described in Architecture & Design Decisions displayed together in one place.
Step 9 — Scan in reciprocal space#
A reciprocal-space scan works like any Bluesky scan — you specify start and stop values in \((h, k, l)\) and hklpy2 converts each step to motor angles automatically.
Set up a minimal Bluesky RunEngine first:
import bluesky.plans as bp
from bluesky import RunEngine
from bluesky.callbacks.best_effort import BestEffortCallback
bec = BestEffortCallback()
bec.disable_plots()
RE = RunEngine({})
RE.subscribe(bec)
Then scan \(h\) from 3.9 to 4.1 around the \((4, 0, 0)\) reflection:
fourc.move(4, 0, 0)
RE(bp.scan([fourc], fourc.h, 3.9, 4.1, 5))
The scan table will show \(h\), \(k\), \(l\) and all four motor angles at each step. Notice that as \(h\) changes, all four motors move together to track the reciprocal-space trajectory — this is what makes a diffractometer different from a simple multi-axis stage.
What you have learned#
In this tutorial we:
Created a simulated 4-circle diffractometer with
creator()Added a silicon sample with
add_sample()Set the X-ray wavelength
Recorded two orientation reflections with
setor()Computed the \(UB\) orientation matrix with
calc_UB()Moved to a reciprocal-space position with
move()Ran a Bluesky scan along a reciprocal-space direction
Where to go next#
How to Use Constraints — set axis limits and cut points to control which
forward()solutions are acceptedHow to Use Presets — hold a real axis at a fixed value during
forward()computationsHow to Choose the Default forward() Solution — choose which
forward()solution the diffractometer uses by defaultExamples — worked demonstrations for specific geometries, EPICS connections, and advanced use cases
Diffractometer — conceptual background on the diffractometer object