4IDG: Diffractometer Usage#
4IDG is the diffraction hutch. The 6-circle diffractometer is controlled through hklpy2 for reciprocal-space navigation. Two configurations are available depending on which insert is mounted:
huber_euler— Huber Eulerian cradlehuber_hp— high-precision goniometer
The HKL utility functions are provided by hkl_utils_hklpy2.py and cover
sample management, orientation, forward/inverse calculations, mode selection,
constraint management, and configuration save/restore.
Command Reference#
Diffractometer and status#
Command |
Description |
|---|---|
|
Select active diffractometer; update motor aliases, h/k/l aliases, and axis constraints |
|
Select Eiger or point detector/analyzer arm |
Moving#
Command |
Description |
|---|---|
|
Current H K L, motor positions, energy, PSI (no parentheses) |
|
Calculate motor angles for HKL without moving |
|
Move to HKL (runs own RunEngine) |
|
Move to HKL as a Bluesky plan |
|
Move gamma and mu together |
Reciprocal-space scans#
Command |
Description |
|---|---|
|
Sweep |
|
Sweep |
|
Sweep |
|
Linear (h, k, l) trajectory |
Peak finding from a previous scan#
Command |
Description |
|---|---|
|
Print peak statistics (com, cen, max, min, fwhm) for the scan’s detectors; supports 1D and 2D |
|
Move scan motor(s) to the FWHM midpoint |
|
Move scan motor(s) to the centroid (center-of-mass) |
|
Move scan motor(s) to the x at peak maximum |
|
Move scan motor(s) to the x at peak minimum |
|
Legacy fallback that reads from the live |
Sample management#
Command |
Description |
|---|---|
|
Add sample interactively |
|
List all samples; mark current one |
|
Switch active sample |
|
Remove a sample |
|
Set lattice constants (no args = interactive) |
|
Refine one lattice constant from current HKL position |
|
Cubic-sample 2θ zero-shift and |
Orientation / UB matrix#
Command |
Description |
|---|---|
|
Set primary reflection at current motor positions |
|
Set secondary reflection at current motor positions |
|
Set primary reflection — prompts for motor positions and HKL |
|
Set secondary reflection — prompts for motor positions and HKL |
|
List all reflections for current sample |
|
List only the two orienting reflections |
|
Interactively pick which reflections are orienting |
|
Swap first and second orienting reflections |
|
Delete a non-orienting reflection |
Modes, azimuth and constraints#
Command |
Description |
|---|---|
|
Set diffractometer mode by number (no args = interactive list) |
|
Set azimuthal reference vector |
|
Freeze constant axis for current mode (no args = use current position) |
|
Interactively freeze all constant axes |
|
Show angle limits and cut point for each axis |
|
Set angle limits and optional cut point (no args = interactive per axis) |
|
Reset all limits to defaults |
Analyzer#
Command |
Description |
|---|---|
|
Select crystal and set d-spacing |
|
Calibrate current motor position to calculated Bragg angle |
Configuration save/restore#
Command |
Description |
|---|---|
|
Save sample, UB, reflections, constraints to YAML |
|
Load configuration from YAML (overwrite or append) |
|
Restore orientation from a previous scan |
Starting the Session#
Use the beamline startup script from a terminal:
bluesky-4idg
This activates the polar-bits conda environment and launches IPython with all
4IDG devices pre-loaded (equivalent to from id4_g.startup import *).
Selecting the Active Diffractometer#
Two diffractometers are available. Call change_diffractometer() to select one.
This sets the active diffractometer for all HKL utility functions, applies the
appropriate axis constraints, and injects motor aliases into the session
namespace:
change_diffractometer("huber_euler") # Eulerian cradle
change_diffractometer("huber_hp") # High-precision goniometer
change_diffractometer() # interactive prompt
Motor aliases created automatically#
After change_diffractometer("huber_euler"):
Alias |
Motor |
|---|---|
|
Diffractometer circles (angular space) |
|
Sample stage |
|
Analyzer arm |
After change_diffractometer("huber_hp"):
Alias |
Motor |
|---|---|
|
Diffractometer circles (angular space) |
|
Xeryon piezo rotation |
|
Sample stage |
|
Nano-focusing stage |
|
Base stage |
|
Analyzer arm |
The reciprocal-space pseudo-axes h, k, l are also injected automatically
into the session namespace by change_diffractometer(). They behave like
ophyd positioners — anywhere a real motor is accepted you can pass h, k,
or l instead, e.g. mv(l, 2.0), lup(l, -0.05, 0.05, 21, 0.5),
ascan(k, -0.1, 0.1, 40, 0.5), or cen(l) after a scan.
Per-Session Startup#
Users typically run a per-session startup file to set additional shortcuts and load experiment-specific settings:
%run startup_4idg.py
change_diffractometer("huber_euler")
change_diffractometer() injects motor aliases and the reciprocal-space
pseudo-axes h, k, l into the session namespace automatically.
Sample Management#
Add a new sample#
newsample() prompts interactively for the sample name and lattice constants.
It also seeds two default reflections — (0 0 2) and (2 0 0) — computes an
initial UB matrix from them, and sets (1 0 0) as the azimuthal reference
vector. These defaults get the diffractometer into a working state immediately;
replace them with measured reflections using or0()/or1() or
setor0()/setor1(), and update the azimuthal reference with setaz() as
needed.
newsample()
List, switch, and remove samples#
sampleList() # list all defined samples; mark current one
sampleChange("Si") # switch to sample named "Si"
sampleChange() # interactive selection
sampleRemove("old_sample") # remove a sample by name
sampleRemove() # interactive selection
Update lattice constants#
Set lattice constants directly or interactively:
setlat(5.43, 5.43, 5.43, 90, 90, 90) # direct: a, b, c, alpha, beta, gamma
setlat() # interactive with current values as defaults
Refine a single lattice constant from the current HKL position (auto-detects which parameter to refine based on position):
update_lattice() # auto: refines a, b, or c depending on current HKL
update_lattice("c") # refine only c
Checking the Current Position#
wh # print H K L, energy, motor positions, PSI — no parentheses
ca(1, 0, 0) # calculate motor angles for (1 0 0) without moving
ca(1, 1, 0) # calculate motor angles for (1 1 0) without moving
wh also snapshots the current reciprocal-space position into uppercase
globals H, K, L so you can reuse them at the prompt:
wh
ubr(H, K, L + 0.01) # step L by +0.01 from the position just printed
Orientation (UB Matrix)#
Setting orientation reflections#
Two workflows exist depending on whether you are at the peak or not.
If you are physically at the peak — use or0() / or1(), which capture
the current motor positions and only ask for H K L:
# Move to the first reflection physically, then:
or0() # record primary reflection at current motors — prompts for H K L
or0(0, 0, 2) # record primary reflection — no prompt
# Move to the second reflection, then:
or1() # record secondary reflection at current motors — prompts for H K L
or1(2, 0, 0) # record secondary reflection — no prompt
To enter motor positions manually — use setor0() / setor1(), which
prompt for both motor positions and H K L:
setor0() # enter angles + H K L for primary reflection
setor1() # enter angles + H K L for secondary reflection
The UB matrix is recalculated automatically whenever the orienting reflections
or lattice parameters are updated — including after or0(), or1(),
setor0(), setor1(), or_swap(), set_orienting(), setlat(), and
update_lattice().
Inspect and manage reflections#
list_reflections() # list all reflections for the current sample
list_orienting() # list only the two orienting reflections
set_orienting() # interactively pick which reflections are orienting
or_swap() # swap first and second orienting reflections; recomputes UB
del_reflection() # interactively delete a non-orienting reflection
Cubic 2θ Zero-Shift and Lattice Constant#
For a cubic sample, theta0() computes the 2θ zero-shift and the lattice
constant a0 from any two stored reflections (Brueckel 1994). It assumes
horizontal scattering geometry (gamma = 2θ, delta = 0).
theta0()
Workflow:
Measure two reflections (e.g. with
or0()/or1()after centering on each Bragg peak).Run
theta0()— it lists all stored reflections, marks the two orienting ones asfirst/second, and prompts for the indices of the two reflections to use (defaults to the orienting pair).The function prints the 2θ zero-shift, two estimates of
a0(one from each reflection), and the corrected 2θ values.
Use the printed zero-shift to update the gamma motor offset, and the
a0 value with setlat() or update_lattice().
Moving in Reciprocal Space#
Move to an HKL position (executes as a Bluesky plan):
RE(br(1, 0, 0)) # move to (1 0 0)
RE(br(2, 0, 0)) # move to (2 0 0)
Move directly without wrapping in RE (runs its own RunEngine):
ubr(1, 0, 0) # move to (1 0 0)
ubr(2, 0, 0) # move to (2 0 0)
Move gamma and mu together (for detector arm alignment):
uan(40, 20) # move to gamma=40, mu=20
Move individual real-space axes with the %mov magic:
%mov phi 5 # move phi to 5°
%mov chi 0
%mov delta 30
The h, k, l pseudo-axes also work with mv, mvr, lup, ascan,
cen, etc. — anywhere a real motor is accepted:
RE(mv(l, 2.0)) # move l to 2.0 (h, k held fixed)
RE(mvr(l, 0.01)) # step l by +0.01
RE(lup(l, -0.05, 0.05, 21, 0.5)) # relative L-scan
RE(ascan(k, -0.1, 0.1, 40, 0.5)) # absolute K-scan
RE(cen(l)) # center l on the last scan's COM
See Scans in reciprocal space for the
dedicated hscan / kscan / lscan / hklscan plans.
Diffractometer Modes#
setmode() lists all available modes (1-indexed). Selecting a mode
automatically freezes the unused detector angle at 0:
setmode() # interactive selection (shows numbered list)
setmode(1) # e.g. "4-circles constant phi horizontal" → freezes delta=0
setmode(7) # e.g. "4-circles bissecting horizontal"
setmode(12) # e.g. "psi constant horizontal"
Available modes include (geometry-dependent):
4-circles constant phi horizontal4-circles constant mu horizontal4-circles constant chi horizontal4-circles bissecting horizontal4-circles constant omega horizontalpsi constant horizontal,psi constant verticalzaxis + alpha-fixed,zaxis + beta-fixed,zaxis + alpha=betalifting detector mu/omega/chi/phi
Azimuth Reference and Constraints#
Set the azimuthal reference vector. This updates the psi-engine counterpart diffractometer and reports the resulting PSI value:
setaz(0, 0, 1) # [001] azimuth reference
setaz(0, 1, 0) # [010] azimuth reference
setaz() # interactive prompt
Freeze the constant axis for the current mode. Behavior depends on mode:
Psi constant modes: freezes psi (uses current psi if no argument)
Single-axis modes (
constant phi/mu/chi): freezes that axis (uses current motor position if no argument)Other modes: interactive prompt for each constant axis
freeze() # auto-detect from current mode; prompt or use current position
freeze(0) # freeze axis at 0 (mode-dependent)
freeze(5) # freeze axis at 5 (mode-dependent)
freeze_general() # always prompts interactively for all constant axes
Show and manage axis constraints (angle limits and cut point). The cut point
defines the wrap-around for the axis: solutions are reported in the interval
[cut, cut + 360).
show_constraints() # print low/high limits and cut point for each axis
reset_constraints() # reset all limits to defaults
set_constraints() # interactive: set limits/cut for each axis
set_constraints("phi") # interactive: set limits/cut for one axis
set_constraints("phi", -180, 180) # set limits only
set_constraints("phi", -180, 180, -180) # set limits and cut point
# Set all 6 axes at once (limits only — 12 args):
set_constraints(-1,1, 0,90, -20,200, -180,180, -2,140, -5,50)
# Set all 6 axes with cut points (limits + cut — 18 args):
set_constraints(
-1, 1, 0,
0, 90, 0,
-20, 200, -180,
-180, 180, -180,
-2, 140, 0,
-5, 50, 0,
)
Interactive prompts accept either two numbers (low high) or three numbers
(low high cut), separated by spaces or commas. Press Enter to keep the
current values.
Inverse Calculation#
Convert real-space motor positions to reciprocal-space coordinates.
Motor order for 6-circle is (gamma, mu, chi, phi, delta, tau):
sol = huber_euler.inverse((40, 20, 90, 0, 0, 0))
print(sol.h, sol.k, sol.l)
Scans#
All plans use counters.detectors and counters.monitor by default
(see General Examples for detector selection).
Scans in angular space#
Scan individual diffractometer circles directly:
# Rock phi around current position (relative scan, 50 pts, 1.0 s dwell)
RE(lup(phi, -1, 1, 50, 1.0))
# Scan delta (2θ) through a Bragg peak
RE(lup(delta, -0.5, 0.5, 50, 0.5))
# Scan chi for polarization dependence
RE(lup(chi, -5, 5, 50, 0.5))
# Absolute scan
RE(ascan(delta, 28, 32, 40, 0.5))
th-2th scan#
th2th is a local plan that scans mu and gamma simultaneously with the
coupled 1:2 ratio. Arguments give the relative 2-theta (gamma) range; mu
moves at half that rate. Positions are restored after the scan (same as lup).
RE(th2th(tth_start, tth_end, number_of_points, time_per_point))
# Examples
RE(th2th(-1, 1, 50, 0.5)) # ±1° in 2θ, 50 pts, 0.5 s/pt
RE(th2th(-2, 2, 100, 0.5)) # ±2° in 2θ, 100 pts
Scans in reciprocal space#
Two equivalent ways to scan a reciprocal-space axis:
Generic plans with the
h,k,laliases (set bychange_diffractometer()) —lup,ascan, etc. work on the pseudo axes the same way they work on real motors:RE(lup(l, 1.8, 2.2, 40, 0.5)) # relative L-scan through (0 0 2) RE(ascan(k, -0.1, 0.1, 40, 0.5)) # absolute K-scan
Dedicated single-axis plans
hscan/kscan/lscan— thin wrappers aroundascanon the matching pseudo axis. They take only(start, stop, num, time)and tag the run with their ownplan_name, which makes them easier to identify in the catalog and works automatically withpeak()’s positioner detection:RE(lscan(1.8, 2.2, 40, 0.5)) # absolute L-scan RE(hscan(1.95, 2.05, 21, 0.5)) # absolute H-scan RE(kscan(-0.05, 0.05, 21, 0.5)) # absolute K-scan
They forward
detectors,lockin,dichro,fixq,vortex_sgz,g_sgz,per_step, andmdtoascan:RE(lscan(1.8, 2.2, 40, 0.5, dichro=True))
Linear (h, k, l) trajectory — hklscan#
hklscan sweeps a straight line in reciprocal space from
(h1, k1, l1) to (h2, k2, l2) in num points. All three pseudo axes
move together; the diffractometer solves the angles at each point.
RE(hklscan(h1, h2, k1, k2, l1, l2, num, time))
# Diagonal scan from (1, 0, 0) to (1, 0, 0.2):
RE(hklscan(1, 1, 0, 0, 0, 0.2, 21, 0.5))
# Off-axis cut crossing (2, 0, 0):
RE(hklscan(1.95, 2.05, -0.02, 0.02, 0, 0, 21, 0.5))
The dichro, lockin, vortex_sgz, g_sgz, per_step, and md
kwargs are forwarded to ascan. fixq is forced off (the scan is the
trajectory).
Center on a peak after a scan:
RE(lup(l, 1.8, 2.2, 40, 0.5))
RE(cen(l)) # moves to the center-of-mass of the last scan
Peak position from a previous scan#
peak_pos, cen, com, maxi, and mini (and the PR-#54 aliases
peak / pmax / pmin) compute peak statistics from a stored scan.
They work on any past run from the 4id_polar catalog — useful for
revisiting a peak later in the session or for picking a specific
detector channel.
Backend: apstools.utils.xy_statistics for the 1D com / max / min
/ fwhm, scipy.signal.find_peaks for the FWHM-midpoint cen, and
scipy.ndimage for true 2D peak detection on grid_scan runs.
Print statistics for the last scan for every detector hinted in the scan:
peak_pos() # last scan, all hinted detectors
peak_pos(-3) # 3 scans back
peak_pos(1234, y="scaler1_ch14") # specific scan, single detector
Move to a peak feature. Wrap in RE():
RE(lup(delta, -0.5, 0.5, 50, 0.5))
RE(cen()) # move delta to the FWHM midpoint
RE(com()) # move delta to the centroid
RE(maxi()) # move delta to x at peak maximum
RE(mini()) # move delta to x at peak minimum
# Pick a specific detector channel:
RE(cen(detector="scaler1_ch14"))
# Operate on an older scan:
RE(cen(scan_id=1234))
# Move a different positioner than the scan axis:
RE(cen(positioner=phi))
cen and com differ only for asymmetric peaks: cen is the midpoint
of the half-max crossings (matches bluesky’s PeakStats.cen), com
is the moment-based centroid Σx·y / Σy.
For multi-motor 1D scans (hklscan, …) the move plans default to the
fastest-changing axis and prompt to confirm; pass confirm=False to
skip every interactive prompt. th2th always uses 2θ (gamma) — no
prompt. psiscan is rejected because the scan axis is a virtual extra,
not a movable positioner.
For 2D grid_scan runs the move plans default to moving both scan
motors in parallel to the 2D feature (issue #59):
RE(grid_scan(cryox, -1, 1, 20, cryoy, -1, 1, 20, 0.2))
RE(cen()) # one mv() moves cryox + cryoy together
RE(maxi(positioner=cryox)) # project to cryox, move only that axis
peak_pos() on a grid_scan returns motor-coordinate tuples
((cryox_val, cryoy_val)) instead of scalars; per-axis fwhm is a
two-element tuple computed from 1D projections along each motor.
If the new plans can’t read a scan (e.g. catalog isn’t reachable),
fall back to the legacy cen2 / maxi2 / mini2. They read from
BestEffortCallback().peaks and only work on cat[-1] (the most
recently plotted run) — but require no catalog access.
For the HP diffractometer sample stage:
RE(lup(x, -0.5, 0.5, 50, 0.1)) # sample X scan
RE(lup(y, -0.5, 0.5, 50, 0.1)) # sample Y scan
RE(lup(nanox, -0.05, 0.05, 50, 0.1)) # nanofocusing X
2D Maps#
Raster scan over two motors:
RE(grid_scan(
cryox, -1, 1, 20,
cryoy, -1, 1, 20,
0.2,
))
RE(rel_grid_scan(
nanoy, -3.5, 3.5, 45,
nanox, -2, 2, 31,
0.05,
snake_axes=True,
))
Analyzer#
Configure and calibrate the analyzer arm. analyzer_configuration() prompts
for the analyzer crystal and sets its d-spacing. analyzer_set() calibrates
the motor position — use it when the analyzer is physically on peak to set the
motor offset so the reported position matches the calculated Bragg angles for
the current energy. Pass "r" to release (clear) the calibration offset:
analyzer_configuration() # select crystal, set d-spacing; optionally pass energy (keV)
analyzer_configuration(7.0) # configure for 7.0 keV
analyzer_set() # calibrate ath/atth offset to calculated Bragg angles (must be on peak)
analyzer_set("r") # release calibration; restore raw motor positions
Detector Selection#
Switch between the Eiger area detector and the point detector/analyzer arm (motors are 25° apart in delta):
set_detector() # interactive: (E)iger or (P)oint Detector/Analyzer
Sample-Area Ringlight#
The sample illuminator is exposed as ringlight and is read into the
baseline stream every scan. The IOC enum has six choices controlled
through convenience methods or set_state:
ringlight.off() # OFF
ringlight.full() # 100%
ringlight.half() # 50%
ringlight.quarter() # 25%
ringlight.eighth() # 12.5%
ringlight.rainbow() # RAINBOW
ringlight.set_state(0) # by index (0-5)
ringlight.set_state("half") # by short name
ringlight.set_state("100%") # by raw IOC label
ringlight.state.get() # read current state ("OFF", "100", "50", ...)
Saving and Restoring Configuration#
Write to file#
Save the current diffractometer state (sample, reflections, UB matrix, constraints, mode) to a YAML file in the current directory:
write_config() # saves to default_polar_config.yml
write_config("EuAl4_run1") # saves to EuAl4_run1_polar_config.yml
write_config("EuAl4_run1", overwrite=True) # skip confirmation prompt
Read from file#
Lists all *_polar_config.yml files in the current directory and prompts
to overwrite or append the current configuration. Restores samples,
azimuthal-reference extras (h2, k2, l2 for psi-constant modes), and
constraints, then recomputes the UB matrix from the orienting reflections.
Wavelength and current mode are intentionally left untouched to avoid
silently retargeting motors.
read_config() # interactive file selection; choose overwrite or append
Restore from a previous scan#
Restore diffractometer orientation from the supplemental data stored in a databroker scan:
restore_huber_from_scan(-1) # most recent scan
restore_huber_from_scan(1234) # scan ID 1234
restore_huber_from_scan(1234, sample_name="EuAl4") # override sample name
restore_huber_from_scan(1234, force=True) # use first available info
Detector Selection#
See General Examples → Detector Selection for
the full counters.plotselect() walkthrough.
counters.plotselect(14, 5) # detector index 14, monitor index 5
counters.plotselect([14, 19], 5) # multiple detectors
Temperature Control#
The recommended path is temperature_setup(label) — it injects three
session globals (tc for the setpoint, ts for the readback, and
TEMPERATURE_CONTROLLER for the active label) and adds ts to the
baseline so the sample temperature lands in every scan automatically:
temperature_setup("g") # LakeShore 336 (default)
mv tc 295 # set the loop-1 setpoint to 295 K
te(295) # equivalent shortcut
ts.get() # current loop-2 readback
RE(count(1, 1, detectors=[ts])) # explicit count
temperature_setup("g-340") # switch to the LakeShore 340
TEMPERATURE_CONTROLLERS lists every supported label — see
id4_common.utils.temperature_setup.
Direct access to the LakeShore device is also available for read-only inspection or non-standard channels:
temp_336_4idg.loop1.readback.get() # sensor A readback (K)
temp_336_4idg.loop1.setpoint.put(100) # set loop-1 setpoint
temp_336_4idg.loop2.readback.get() # sensor B readback (K)
temp_340_4idg.sample.get() # readback of LS340 "sample" channel
temp_340_4idg.control.setpoint.put(295) # set LS340 control loop
Saving Data#
SPEC files are enabled by default. Use newSpecFile to start a new file and
spec_comment to annotate the logbook:
newSpecFile("EuAl4_experiment")
# → creates e.g. 0410_EuAl4_experiment.dat
spec_comment("Sample: EuAl4, oriented (001), T = 300 K")
spec_comment("Aligned at (2 0 0), phi frozen at 5 deg")
Access recent runs from the databroker catalog:
run = cat[-1] # most recent run
run.primary.read() # read as xarray Dataset
polartools provides higher-level routines for diffraction data analysis, available in the session namespace:
df = load_table(-1, cat)
fit = fit_peak(df["delta"], df["scaler1_ch14"])
plot_fit([10, 20, 1], cat, positioner="delta", detector="scaler1_ch14")
fit_series([10, 20, 1], cat, positioner="delta", detector="scaler1_ch14")
mesh = load_mesh(-1, cat, xmotor="cryox", ymotor="cryoy", detector="scaler1_ch14")
plot_2d(mesh)