Refine the Lattice Constants#
This guide shows how to refine unit-cell parameters from a set of measured Bragg peak positions. Two methods are available:
Method |
Function |
Algorithm |
When to use |
|---|---|---|---|
Busing & Levy (1967) |
|
Iterative least-squares with analytic Jacobian |
Good starting point; can refine cell and orientation simultaneously |
Nelder-Mead simplex |
|
Derivative-free simplex |
Far from solution; irregular residual surface; orientation held fixed |
Both methods support constrained refinement (crystal-system symmetry enforced) and unconstrained refinement (all six parameters free).
Prerequisites#
A geometry with a wavelength set
A UB matrix set on the sample (see Orient a Crystal)
At least three measured reflections with accurate motor angles
More reflections give a more reliable result; six or more are recommended.
Set up the geometry and collect reflections#
import ad_hoc_diffractometer as ahd
g = ahd.presets.fourcv()
g.wavelength = 1.5498 # Å
g.sample.lattice = ahd.Lattice(a=4.785, c=12.991, gamma=120.0) # sapphire
# Set UB from two orienting reflections (see howto/orient)
ahd.ub_from_two_reflections_bl1967(
g.sample,
r1=g.reflections["r1"],
r2=g.reflections["r2"],
)
# Collect additional measured reflections for refinement
measured = [
("r1", (0, 0, 6), {"ttheta": 41.937, "omega": 20.931, "chi": 90.032, "phi": 0.0}),
("r2", (1, 0, 4), {"ttheta": 33.508, "omega": 16.714, "chi": 76.074, "phi": 0.0}),
("r3", (0, 1, 2), {"ttheta": 27.823, "omega": 13.882, "chi": 57.587, "phi": 90.0}),
("r4", (1, 1, 0), {"ttheta": 36.874, "omega": 18.449, "chi": 76.989, "phi": 45.0}),
("r5", (0, 0, 12), {"ttheta": 91.371, "omega": 45.657, "chi": 90.061, "phi": 0.0}),
("r6", (2, 0, 0), {"ttheta": 40.181, "omega": 20.053, "chi": 90.034, "phi": 30.0}),
]
for name, hkl, angles in measured:
g.reflections.add(name, hkl=hkl, angles=angles)
rlist = [g.reflections[name] for name, *_ in measured]
Method 1 — Busing & Levy least-squares#
Iteratively solves the linearized normal equations to minimize the RMS misfit between observed and calculated phi-frame scattering vectors. Can refine cell parameters and orientation (U matrix) simultaneously.
Step 1 — Constrained (crystal-system symmetry enforced)#
result1 = ahd.refine_lattice_bl1967(
g.sample,
rlist,
refine_cell=True,
refine_orientation=True, # also refine U
refine_all=False, # enforce hexagonal constraints
)
lat1 = result1["lattice"]
print(f"a = {lat1.a:.6f} Å c = {lat1.c:.6f} Å gamma = {lat1.gamma:.4f}°")
print(f"RMS misfit : {result1['rms']:.3e}")
print(f"Converged : {result1['converged']} ({result1['n_iter']} iterations)")
Step 2 — Unconstrained (all six parameters free)#
g.sample.lattice = result1["lattice"]
result2 = ahd.refine_lattice_bl1967(
g.sample,
rlist,
refine_all=True, # all six parameters independent
)
lat2 = result2["lattice"]
print(f"a = {lat2.a:.6f} b = {lat2.b:.6f} c = {lat2.c:.6f}")
print(f"α = {lat2.alpha:.4f}° β = {lat2.beta:.4f}° γ = {lat2.gamma:.4f}°")
print(f"RMS misfit : {result2['rms']:.3e}")
Method 2 — Nelder-Mead simplex#
Derivative-free global minimisation. More robust when the starting point is far from the solution, but slower and does not refine U by default. Use this as a first pass when the BL1967 method fails to converge.
Step 1 — Constrained#
result1 = ahd.refine_lattice_simplex(
g.sample,
rlist,
refine_all=False, # enforce crystal-system constraints
)
lat1 = result1["lattice"]
print(f"a = {lat1.a:.6f} Å c = {lat1.c:.6f} Å gamma = {lat1.gamma:.4f}°")
print(f"RMS misfit : {result1['rms']:.3e}")
print(f"Converged : {result1['converged']} ({result1['n_iter']} evaluations)")
Step 2 — Unconstrained#
g.sample.lattice = result1["lattice"]
result2 = ahd.refine_lattice_simplex(
g.sample,
rlist,
refine_all=True,
)
lat2 = result2["lattice"]
print(f"a = {lat2.a:.6f} b = {lat2.b:.6f} c = {lat2.c:.6f}")
print(f"α = {lat2.alpha:.4f}° β = {lat2.beta:.4f}° γ = {lat2.gamma:.4f}°")
print(f"RMS misfit : {result2['rms']:.3e}")
Interpreting the results#
Quantity |
Meaning |
|---|---|
|
RMS misfit between observed and predicted Q-vectors (Å⁻¹) |
|
|
|
Iterations (BL1967) or function evaluations (simplex) used |
|
Per-reflection misfit vector |
Constrained vs unconstrained:
Always start with constrained refinement (refine_all=False). It uses
fewer free parameters, converges faster, and enforces the known crystal
symmetry. Use unconstrained refinement (refine_all=True) as a
diagnostic: if b deviates significantly from a, or γ from 120°, this
may indicate a real distortion, an indexing error, or miscalibrated angles.
Choosing a method:
Start with BL1967 — it is faster and also refines the orientation.
If BL1967 does not converge (poor starting lattice, highly distorted cell), use simplex first to get close, then hand off to BL1967 for final polishing.