Forward and Inverse Computations#
The diffractometer has two complementary computations:
Forward (
g.forward(h, k, l)) — given Miller indices, find the motor angles that satisfy the Bragg condition (hkl → motor angles).Inverse (
g.inverse(**angles)) — given a set of motor angles, find the Miller indices of the reflection currently in the Bragg condition (motor angles → hkl).
This guide covers the forward computation. The inverse is used, for example, after manually positioning the diffractometer to identify an unknown peak.
Prerequisites#
A geometry with a wavelength set
A sample with a UB matrix set (see Orient a Crystal)
Set a diffraction mode#
Before calling forward(), set the active diffraction mode. The mode
controls which stages are free, fixed, or coupled, and determines which
solutions are physically meaningful:
import ad_hoc_diffractometer as ahd
g = ahd.presets.fourcv()
g.wavelength = 1.5406 # Å
g.sample.lattice = ahd.Lattice(a=5.431)
ahd.ub_identity(g.sample)
g.mode_name = "bisecting" # omega = ttheta/2; standard synchrotron mode
For fixed-angle modes, preset the stage angle before activating the mode:
g.set_angle("chi", 90.0) # preset chi to the desired fixed value
g.mode_name = "fixed_chi" # chi will be held at 90° during forward()
Without a mode, all stages are free and the solver returns all geometrically valid solutions. See Switch Diffraction Modes for the full list.
Forward computation#
solutions = g.forward(1, 1, 0)
for s in solutions:
print(s)
forward() returns a list of dicts, each mapping stage name → angle
(degrees). Multiple solutions exist because the Bragg condition fixes only
the direction of the scattering vector Q in the laboratory frame — it
does not uniquely determine the motor angles. For a four-circle geometry,
the same Q can typically be reached with chi > 0 or chi < 0 (two
branches), and for each branch there are infinitely many (omega, phi) pairs
that satisfy the condition. The solver samples a finite set of
representative solutions; the active diffraction mode filters these to those
that satisfy any additional constraints. It is the caller’s responsibility
to select the physically appropriate solution for their experimental setup.
Select a solution#
# Take the first solution
angles = g.forward(1, 1, 0)[0]
print(angles)
# {'omega': 23.65, 'chi': 35.26, 'phi': 0.0, 'ttheta': 47.30}
Predict the Bragg angle only#
To get d-spacing and 2θ without computing motor angles:
d = ahd.hkl_to_d(g, 1, 1, 0)
tth = ahd.hkl_to_two_theta(g, 1, 1, 0)
print(f"d = {d:.4f} Å, 2θ = {tth:.3f}°")
Apply motor limits#
Stage limits are enforced automatically by forward(). To set limits:
g.stage("chi").limits = (-10.0, 100.0) # degrees
Solutions outside the limits are filtered out.
Inverse computation#
inverse() requires a UB matrix to be set on the sample (see Orient a Crystal).
Given a set of motor angles, it returns the Miller indices of the reflection
currently in the Bragg condition.
Note
inverse() always returns a unique hkl: once UB is established, a single
matrix multiplication maps motor angles unambiguously to reciprocal space.
forward() is the reverse: the Bragg condition constrains only the direction
of Q in the laboratory frame, not the individual motor angles, so the
result is a list that may contain anywhere from 0 (reflection
unreachable) to a geometry-dependent maximum — typically 2–12 solutions for
four- and six-circle geometries depending on the number of free stages and
the active diffraction mode.
# After driving the diffractometer to some position manually:
hkl = g.inverse(omega=23.65, chi=35.26, phi=0.0, ttheta=47.30)
print(hkl) # (h, k, l) as a numpy array
This is useful for identifying an unknown peak found during a scan, or for verifying that the diffractometer is positioned at the expected reflection after moving motors.
Handle errors from forward()#
forward() raises specific exceptions for distinct failure modes:
EwaldSphereViolation#
Raised when the requested reflection cannot be reached at the current
wavelength — |Q| > 4π/λ regardless of motor angles:
from ad_hoc_diffractometer import EwaldSphereViolation
try:
solutions = g.forward(10, 10, 10)
except EwaldSphereViolation as e:
print(f"|Q| = {e.q_mag:.4f} Å⁻¹")
print(f"Ewald sphere limit = {e.q_max:.4f} Å⁻¹ (λ = {e.wavelength} Å)")
print("Reduce wavelength or choose a smaller reflection.")
Attributes: q_mag (requested |Q| in Å⁻¹), q_max (4π/λ), wavelength.
ConstraintViolation#
Raised when the solver returns a solution that violates a declared constraint beyond the display-precision tolerance. This signals either a solver bug or an unimplemented virtual-angle constraint:
from ad_hoc_diffractometer import ConstraintViolation
try:
solutions = g.forward(1, 0, 0)
except ConstraintViolation as e:
print(f"Solution {e.solution_index} violated {e.constraint_repr}")
print(f"Residual = {e.residual:.2e}° (tolerance = {e.tolerance:.2e}°)")
Attributes: solution_index, constraint_repr, residual, tolerance.
NotImplementedError#
Raised when the active mode is None or its is_implemented(geometry)
returns False (e.g. a prerequisite like azimuthal_reference or
surface_normal is not set):
g.mode_name = None
try:
g.forward(1, 0, 0)
except NotImplementedError as e:
print(e)
# fixed_psi requires azimuthal_reference to be set
g.mode_name = "fixed_psi"
g.azimuthal_reference = None
try:
g.forward(1, 0, 0)
except NotImplementedError as e:
print(e) # describes which prerequisite is missing