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

See also#