4IDG: Diffractometer Usage#

4IDG is the diffraction hutch. The primary instrument is the Huber Euler 6-circle diffractometer (huber_euler), controlled through hklpy for reciprocal-space navigation. An HP (high-pressure) diffractometer (huber_hp) is also available.


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 *).


Motor Shortcuts#

After the main startup, users typically run a per-session startup file (e.g. startup_4idg.py) that assigns convenient short names to frequently used motors:

from apsbits.core.instrument_init import oregistry

huber_euler = oregistry.find("huber_euler")
huber_hp    = oregistry.find("huber_hp")
energy      = oregistry.find("energy")
gkb         = oregistry.find("gkb")

# Motor shortcuts for huber_euler
sx  = huber_euler.x
sy  = huber_euler.y
sz  = huber_euler.z
mu  = huber_euler.mu
delta = huber_euler.delta
chi   = huber_euler.chi
phi   = huber_euler.phi
gamma = huber_euler.gamma

# Motor shortcuts for huber_hp (nanofocusing stage)
nanox = huber_hp.nanox
nanoy = huber_hp.nanoy
sx_hp = huber_hp.x
sy_hp = huber_hp.y

Run this file at the start of each session:

%run startup_4idg.py

Selecting the Active Diffractometer#

Two diffractometer geometries are available. Select the one in use before running any HKL calculations or moves:

select_diffractometer(huber_euler)   # Cradle Euler (most common)
select_diffractometer(huber_hp)      # High-pressure cell

Defining a Sample#

Create a sample with its unit cell parameters (a, b, c in Å; α, β, γ in °). sampleNew prompts interactively if called without arguments, or accepts positional arguments:

# Interactive (prompts for name and lattice constants)
sampleNew()

# Non-interactive
sampleNew("Si", 5.4, 5.4, 5.4, 90, 90, 90)          # cubic Si
sampleNew("MyOxide", 3.84, 3.84, 12.6, 90, 90, 90)   # tetragonal

Verify the current sample and orientation matrix:

wh     # print current reciprocal-space position and sample info (no parentheses needed)
pa()   # print full engine parameters: UB matrix, reflections, mode

Orientation (UB Matrix)#

Set orientation reflections one at a time. Move to the peak physically, then call setor0() / setor1() to record the reflection at the current motor positions:

# Move to the first reflection physically, then:
setor0()    # record orientation reflection 0 — prompts for H K L if not at peak

# Move to the second reflection, then:
setor1()    # record orientation reflection 1 — UB matrix is now calculated

List stored reflections:

list_reflections()

Checking Angles and Moving in Reciprocal Space#

Calculate motor angles for any HKL without moving:

ca(1, 0, 0)     # angles for (1 0 0)
ca(1, 1, 0)     # angles for (1 1 0)
ca(0, 0, 2)     # angles for (0 0 2)

Move to an HKL position (executes as a plan):

RE(br(1, 0, 0))     # move to (1 0 0)
RE(br(2, 0, 0))     # move to (2 0 0)
RE(br(1, 1, 0))     # move to (1 1 0)
wh                  # confirm position after move

Move individual real-space axes with the %mov magic:

%mov huber_euler.phi 5        # move phi to 5°
%mov huber_euler.chi 0        # move chi to 0°
%mov huber_euler.delta 30     # move delta to 30°

# Or using shortcuts:
%mov phi 5
%mov chi 0

Diffractometer Modes#

setmode() opens an interactive prompt listing all available modes. Pass a mode number to set it directly:

setmode()      # interactive selection
setmode(7)     # e.g. "4-circles bissecting horizontal"
setmode(12)    # e.g. "psi constant horizontal"

Available modes include (geometry-dependent):

  • 4-circles constant phi horizontal

  • zaxis + alpha-fixed, zaxis + beta-fixed, zaxis + alpha=beta

  • 4-circles bissecting horizontal

  • 4-circles constant omega horizontal

  • psi constant horizontal

  • lifting detector mu/omega/chi/phi


Azimuth Reference and Constraints#

Set the azimuth reference vector (interactive if called without arguments):

setaz(0, 0, 1)   # [001] azimuth reference
setaz(0, 1, 0)   # [010] azimuth reference

Freeze an axis at a fixed value (freezes phi by default; interactive if no argument given):

freeze(0)     # freeze phi at 0°
freeze(5)     # freeze phi at 5°
freeze(-90)   # freeze phi at -90°

Show and manage axis constraints:

show_constraints()    # print current limits and frozen values
reset_constraints()   # reset all to defaults
set_constraints()     # interactive loop — set low/high limits per axis

Inverse Calculation#

Convert real-space motor positions to reciprocal-space coordinates:

# (mu, delta, gamma, chi, phi, omega) → (h, k, l)
sol = huber_euler.inverse((0, 30, 0, 0, 0, 69.0966))
print(sol.h, sol.k, sol.l)

sol = huber_euler.inverse((0, 92, 33.2, -139, 33.1, 0))
print(sol)

Scanning in Reciprocal Space#

Scan along real-space motors while monitoring intensity with the scalers. All plans use counters.detectors and counters.monitor by default (see General Examples for detector selection):

# 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))

For the HP diffractometer sample stage:

RE(lup(sx_hp, -0.5, 0.5, 50, 0.1))    # sample X scan
RE(lup(sy_hp, -0.5, 0.5, 50, 0.1))    # sample Y scan

Center on a peak after a scan:

RE(lup(phi, -1, 1, 50, 0.5))
RE(cen(phi))     # moves phi to the center-of-mass of the last scan

2D Maps#

Raster scan over two motors:

# Grid scan over sample position (useful for mapping or alignment)
RE(grid_scan(
    sx, -1, 1, 20,
    sy, -1, 1, 20,
    0.2,    # dwell time per point
))

# Relative grid scan with snake trajectory
RE(rel_grid_scan(
    nanoy, -3.5, 3.5, 45,
    nanox, -2, 2, 31,
    0.05,
    snake_axes=True,
))

Detector Selection#

See General Examples → Detector Selection for the full counters.plotselect() walkthrough.

# Quick selection: detector index 14, monitor index 5
counters.plotselect(14, 5)

# Multiple detectors
counters.plotselect([14, 19], 5)

Temperature Control#

Temperature controllers are available at 4IDG:

# LakeShore 336 (primary)
temp_336_4idg.input.A.temperature.get()     # read sensor A
temp_336_4idg.output.one.setpoint.put(100)  # set to 100 K

# LakeShore 340
temp_340_4idg.input_A.get()

# Convenient shortcut (from per-session startup)
temp = oregistry.find("temp_340_4idg").control

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:

# Fit a single peak: load data first, then fit the arrays
df = load_table(-1, cat)
fit = fit_peak(df["delta"], df["scaler1_ch14"])

# Fit and plot a series of scans (scan IDs as start, stop, step triplet)
# e.g. scans 10 through 20 in steps of 1
plot_fit([10, 20, 1], cat, positioner="delta", detector="scaler1_ch14")
fit_series([10, 20, 1], cat, positioner="delta", detector="scaler1_ch14")

# Load and display a 2D mesh scan
mesh = load_mesh(-1, cat, xmotor="sx", ymotor="sy", detector="scaler1_ch14")
plot_2d(mesh)