Concepts#
Key ideas behind ad_hoc_diffractometer. Each section gives a brief
overview and links to richer detail in the how-to guides and background
pages.
Coordinate convention#
Diffractometer stages are described in terms of three observable physical directions that can be identified directly in the laboratory:
Physical direction |
Lab meaning |
|---|---|
vertical |
opposite to gravitational acceleration |
longitudinal |
a chosen direction in the plane perpendicular to vertical, conventionally aligned with the nominal incident beam; a property of the instrument installation |
transverse |
orthogonal to both; positive sense completes a right-handed system (vertical × longitudinal) |
The package uses a right-handed Cartesian frame internally. Different authors
assigned different Cartesian letters (x, y, z) to these physical directions —
historically a source of confusion when diffractometer geometries are compared.
The package accepts any right-handed orthogonal basis via the basis argument
to AdHocDiffractometer.
Physical direction |
Cartesian |
Constant |
|---|---|---|
vertical |
+x |
|
longitudinal |
+y |
|
transverse |
+z |
|
Used by: psic, sixc, kappa6c, zaxis, s2d2, fivec
Pass basis=BASIS_YOU (the default for these geometries).
Physical direction |
Cartesian |
Constant |
|---|---|---|
vertical |
+z |
|
longitudinal |
+y |
|
transverse |
+x |
|
Convention of Busing & Levy.
Used by: fourcv, fourch, kappa4cv, kappa4ch
Also used by:
Pass basis=BASIS_BL (the default for these geometries).
Physical direction |
Cartesian |
Constant |
|---|---|---|
vertical |
+y |
|
longitudinal |
+z |
|
transverse |
+x |
|
Used by: NeXus
Also used by:
Physical direction |
Cartesian |
Constant |
|---|---|---|
vertical |
+z |
|
longitudinal |
+x |
|
transverse |
+y |
|
Used by: Hkl
The BASIS_YOU and BASIS_BL constants are exported from the package.
Axis sign convention#
Each stage’s rotation axis is a signed unit vector: +nHat means
right-handed rotation, -nHat means left-handed (equivalent to
right-handed about the negated axis). Physical direction names
("vertical", "transverse", "longitudinal") are resolved against
the geometry’s basis dict.
The two equivalences in full:
For stages where a published convention uses a left-handed sense (for example eta, phi, and delta in You 1999 about the transverse axis), the package stores the signed axis vector as \(-\hat{n}\) rather than \(+\hat{n}\). The physical rotation axes are the same; only the sign convention for positive rotation differs.
See parse_axis().
Stage stacking#
Stages are stacked: each stage sits on its parent and its rotation modifies
the orientation of everything above it. The parent attribute names the
stage directly below (None for floor-mounted stages). The combined sample
rotation matrix is the ordered product from floor to innermost stage.
See Stage.
Declarative geometries#
A diffractometer geometry can be described in two ways:
Declaratively, as a YAML file that lists the basis, the stages, and the diffraction modes. This is the recommended approach for any geometry that fits the schema. The 10 demo geometries shipped under
src/ad_hoc_diffractometer/geometries/are written in this form; they are demonstrations of the schema rather than authoritative descriptions of any production diffractometer.Programmatically, as a Python factory function that returns an
AdHocDiffractometer. Use this when stage axes depend on a runtime parameter the schema does not express, or when the stage layout itself is computed.
Every declarative file begins with the schema marker
ad_hoc_diffractometer_geometry: {schema_revision: 1}, which
identifies the document and selects the schema revision. The loader
(load_geometry_file() and
register_geometry_file())
parses the YAML, validates it, and constructs the geometry. See
Declarative Geometry Schema for the complete reference and
Build a Custom Diffractometer Geometry for a task-oriented walkthrough.
Monochromatic radiation#
The package assumes monochromatic radiation throughout — all calculations are performed at a fixed wavelength. Energy and wavelength are related by \(hc = 12.3984\,\text{keV·Å}\) exactly (2019 SI redefinition).
g.wavelength = 1.5406 # Å (Cu Kα)
See Set Wavelength / Energy and radiation.
The B, U, and UB matrices#
Three matrices connect Miller indices to motor angles:
Symbol |
Name |
Role |
|---|---|---|
B |
B matrix |
Encodes the reciprocal lattice; maps hkl → crystal Cartesian frame |
U |
U matrix |
Orthonormal; encodes crystal mounting on the diffractometer |
UB |
UB matrix |
Maps hkl → phi-axis frame; determined from orienting reflections |
The B matrix is constructed from unit-cell parameters \((a, b, c, \alpha, \beta, \gamma)\). U is determined by measuring two or more Bragg reflections. UB = U × B maps Miller indices directly to the phi-axis frame.
B matrix#
The B matrix (Busing & Levy 1967, eq. 3) transforms Miller indices \(\mathsf{h} = (h, k, l)^{\mathsf{T}}\) to the scattering vector in Cartesian crystal-frame coordinates:
B is constructed from the reciprocal lattice parameters derived from \((a, b, c, \alpha, \beta, \gamma)\) and is not in general orthonormal. See Direct Lattice in Crystallography for the explicit construction. Note that B depends only on the reciprocal lattice and is independent of the basis assignment for the diffractometer axes.
U matrix#
The U matrix (Busing & Levy 1967, eq. 4) is the orthogonal matrix relating the phi-axis frame (attached to the innermost sample stage) to the crystal Cartesian frame:
U corrects for the misalignment between the crystal axes and the diffractometer axes when all motor angles are zero. Unlike B, U does depend on the basis assignment — choosing a different right-handed basis for the diffractometer axes rotates the U matrix by the fixed signed-permutation matrix that relates the two bases. The physical Bragg condition is invariant under that change; see Case Study: Choice of Basis and the UB Matrix for the worked case study.
Nomenclature#
To avoid the ambiguity noted by Walko (2016) — where both U and UB are sometimes called the “orientation matrix” — this package uses the following unambiguous names:
Symbol |
Name |
Meaning |
|---|---|---|
B |
B matrix |
Maps Miller indices to crystal Cartesian coords; encodes \(a, b, c, \alpha, \beta, \gamma\) |
U |
U matrix |
Orthonormal; relates the crystal Cartesian frame to the phi-axis frame |
UB |
UB matrix |
Maps Miller indices directly to the phi-axis frame; determinable from reflections alone |
UB as a practical entity#
Busing & Levy treat UB as a single practical entity (eqs. 29–31):
where \(\mathsf{H}_c\) and \(\mathsf{H}\) are matrices of observed and indexed reflection vectors respectively. This allows UB to be determined even when lattice parameters are unknown.
Full diffraction equation#
The full diffraction equation (You 1999, eqs. 10–11) relates Miller indices \(\mathsf{h}\) to the sample rotation matrices and the detector position:
where \(U_\mu\), \(X\), \(H\), \(M\) are the motor rotation matrices for mu, eta, chi, and phi respectively, and \(\mathsf{h}^M\) is the diffraction vector in the laboratory frame.
The detector position is determined by:
where \(D\) and \(P\) are the rotation matrices for delta and nu, \(k = 2\pi/\lambda\) is the wave number, and \(\mathsf{k}_{f0}\) is the forward beam direction.
Note
You (1999) uses the symbol U for both the mu motor rotation matrix and the U (orientation) matrix. This package writes the mu motor rotation as \(U_\mu\) to avoid the ambiguity.
See Orient a Crystal, Define the Sample Lattice,
Lattice, and the
Case Study: Choice of Basis and the UB Matrix case study showing that two different right-handed
basis assignments applied to the same equipment produce U/UB
matrices related by a fixed rotation while leaving the physics
invariant.
Diffraction modes#
A diffraction mode is a ConstraintSet
that describes how forward() resolves the free degrees of freedom: which
stages are fixed, which are coupled, and which are solved freely.
Available modes depend on the geometry.
# Four-circle geometries use "bisecting"
g = ahd.make_geometry("fourcv")
g.mode_name = "bisecting"
# Six-circle psic uses named variants
g = ahd.make_geometry("psic")
g.mode_name = "bisecting_vertical" # vertical scattering plane
g.mode_name = "bisecting_horizontal" # horizontal scattering plane
Modes can also be added at run time:
from ad_hoc_diffractometer import ConstraintSet, SampleConstraint
g.modes["my_chi45"] = ConstraintSet([SampleConstraint("chi", 45.0)])
g.mode_name = "my_chi45"
See Switch Diffraction Modes, Work with Constraints and Diffraction Modes, and
mode.
Diffraction constraints#
Specifying (h, k, l) provides exactly 3 equations on the motor angles.
A geometry with N real axes therefore has N − 3 free parameters that must
each be resolved by a constraint. Every mode is a
ConstraintSet — an ordered list of
constraints equal in length to N − 3.
Three constraint categories exist:
Sample constraints fix one sample motor angle at a declared value, or express the bisecting relational condition:
from ad_hoc_diffractometer import SampleConstraint, BisectConstraint
SampleConstraint("chi", 90.0) # chi fixed at 90°
BisectConstraint("eta", "delta") # eta = delta / 2 (psic bisecting)
Detector constraints fix one detector stage at a declared value, or
constrain the azimuthal angle of Q (the "qaz" pseudo-angle from You 1999
eq. 18: tan(qaz) = tan(delta) / sin(nu)):
from ad_hoc_diffractometer import DetectorConstraint
DetectorConstraint("nu", 0.0) # nu fixed at 0°
DetectorConstraint("qaz", 90.0) # Q in the vertical plane
Reference constraints express a condition between Q and an external reference vector n̂ (surface normal, polarization axis, etc.):
from ad_hoc_diffractometer import ReferenceConstraint
ReferenceConstraint("alpha_i", 5.0) # incidence angle fixed
ReferenceConstraint("a_eq_b", True) # alpha_i = beta_out (symmetric)
Taxonomy rules: at most one DetectorConstraint,
at most one ReferenceConstraint.
Two checks distinguish solver availability from prerequisite satisfaction:
constraint.is_implemented(geometry)— returnsTruewhen a forward solver exists for this constraint on this geometry.rc.has_reference_vector(geometry)— returnsTruewhen the required n̂ vector is set on the geometry (a prerequisite for reference constraints, independent of solver availability).
Kappa virtual angles#
Kappa geometries (kappa4cv, kappa4ch, kappa6c) have real
motor angles (komega, kappa, kphi) and virtual
Eulerian pseudoangles (omega, chi, phi) that are more
intuitive to specify.
Geometry-aware decomposition#
The conversion is derived directly from the preset’s actual signed stage axes via the rotation-matrix identity
Each kappa preset declares the four signed axis vectors
(n_komega, n_kappa, n_kphi, n_chi_eq) in a
KappaPseudoAngleConvention
instance attached to geometry.kappa_pseudo_angle_convention.
The conversion functions
eulerian_to_kappa_axes() and
kappa_to_eulerian_axes() solve the
identity above analytically — no Newton iteration is required:
from ad_hoc_diffractometer.kappa import (
eulerian_to_kappa_axes, kappa_to_eulerian_axes,
)
g = ahd.make_geometry("kappa4cv")
convention = g.kappa_pseudo_angle_convention
# Virtual Eulerian angles → real kappa motor angles (two branches)
komega, kappa, kphi = eulerian_to_kappa_axes(
omega, chi, phi, convention, branch=+1
)
# Real kappa angles → virtual Eulerian pseudoangles
omega, chi, phi = kappa_to_eulerian_axes(komega, kappa, kphi, convention)
Branch selection: branch=+1 (default) returns the kappa solution
with the smaller |κ| (the natural identity branch); branch=-1
returns the chi-mirrored solution.
The kappa rotation axis itself is computed per preset to match the published reference figures (issue #252). In physical-direction language the kappa axis is inclined by α from the omega axis, lying in the plane that contains both omega and the equivalent-Eulerian chi axis, and tilted from omega toward that chi direction (Walko 2016 §4.1; ITC Vol. C §2.2.6.2: “the κ axis is inclined at 50° to the ω axis”). Per preset:
Preset |
omega line |
chi-eq line |
Axis vector |
|---|---|---|---|
|
transverse |
+vertical |
\(\hat{n}_{\kappa} = \cos\alpha \cdot \hat{T} + \sin\alpha \cdot \hat{V}\) |
|
vertical |
+longitudinal |
\(\hat{n}_{\kappa} = \cos\alpha \cdot \hat{V} + \sin\alpha \cdot \hat{L}\) |
|
transverse |
+vertical |
\(\hat{n}_{\kappa} = \cos\alpha \cdot \hat{T} + \sin\alpha \cdot \hat{V}\) (same as |
For kappa4cv and kappa6c this matches Walko (2016) Figure 3
and Thorkildsen et al. (2006) Table 1; for kappa4ch it
matches Wyckoff (1985) Figure 2(b) on p. 334.
Note that the kappa axis vector is not in general equal to
cos α · n_komega + sin α · n_chi_eq: the omega axis carries a
signed handedness (e.g. komega = −TRANSVERSE for left-handed
omega about the transverse line), while the kappa axis is computed
from the unsigned basis-direction lines so the kappa arm
physically extends into the upper half-space toward the published
direction. The four-axis kappa pseudoangle convention stored in
KappaPseudoAngleConvention
records the actual signed stage axes; the closed-form solver in
kappa.py works directly from those signed axes and does not
rely on the algebraic identity above.
Divergence from Walko (2016) eq. [16]#
The original Walko closed form
is correct only for the axis convention assumed in Walko’s
derivation — omega about the transverse axis, chi about the
longitudinal axis, phi about the transverse axis, all with a
specific handedness. No preset shipped with this package matches
that convention exactly: kappa4cv (BL) places komega along
−TRANSVERSE; kappa4ch (BL) along −VERTICAL; kappa6c
(You) along −TRANSVERSE with a horizontal mu base. The
textbook formula therefore does not preserve the scattering
vector for any non-zero chi in any of these presets, which
manifested as silent "No solutions" returns from the kappa
virtual-angle solver (issue #241).
The textbook helpers
eulerian_to_kappa() and
kappa_to_eulerian() are retained
as reference implementations of the published closed form (with
deprecation warnings in their docstrings) but are not used
inside the solver.
Handedness convention (Walko vs. ITC §2.2.6.2)#
The presets shipped with this package follow Walko’s left-handed
sign convention for omega/kphi/2theta (encoded as
−TRANSVERSE or −VERTICAL in the Stage axis vectors).
ITC Vol. C §2.2.6.2 (2006) instead specifies the standard signs
of omega/chi/phi as right-handed (only 2θ is left-handed
in Hamilton’s choice). The two conventions are equivalent up to
motor-angle sign flips and yield the same physical orientations.
Users who prefer the ITC convention can construct their own
geometry by negating the relevant Stage axis vectors — see
register_geometry() for the
preset-construction API.
Modes accept virtual angle names#
Kappa modes accept the virtual angle names directly in
SampleConstraint:
from ad_hoc_diffractometer import ConstraintSet, SampleConstraint
g = ahd.make_geometry("kappa4cv")
# "chi" is a virtual angle — the kappa inversion solver handles it
g.modes["fixed_chi"] = ConstraintSet([SampleConstraint("chi", 90.0)])
See eulerian_to_kappa_axes(),
kappa_to_eulerian_axes(),
KappaPseudoAngleConvention,
and the Work with Constraints and Diffraction Modes guide.
Surface geometry and reference vector#
Some diffraction modes and pseudo-angle functions require an external reference vector supplied as Miller indices (h, k, l). Two separate vectors may be set:
surface_normal— direction perpendicular to the sample surface; used by incidence/exit angle functions and surface diffraction modes.azimuthal_reference— direction defining ψ = 0; used bypsi_angleandfixed_psi_*modes.
g.surface_normal = (0, 0, 1) # (001)-cut sample
g.azimuthal_reference = (1, 0, 0)
Vectors are stored as Miller indices and converted to the lab frame internally via the UB matrix.
See Surface Geometry and the Reference Vector and reference.
Custom exceptions#
Two exceptions signal specific failure modes of the forward solver:
EwaldSphereViolation
: Raised when |Q| > 4π/λ — the requested reflection cannot be reached at
the current wavelength regardless of motor angles. Carries attributes
q_mag, q_max, and wavelength.
ConstraintViolation
: Raised when a solver returns a solution that violates a declared
constraint beyond the display-precision tolerance (indicates a solver
error or an unimplemented virtual-angle constraint). Carries attributes
solution_index, constraint_repr, residual, and tolerance.
from ad_hoc_diffractometer import EwaldSphereViolation, ConstraintViolation
try:
solutions = g.forward(10, 10, 10) # likely unreachable
except EwaldSphereViolation as e:
print(f"|Q| = {e.q_mag:.3f} Å⁻¹ exceeds Ewald sphere (max {e.q_max:.3f} Å⁻¹)")
See Forward and Inverse Computations and mode.
Forward and inverse computations#
The ψ angle#
Two definitions of ψ appear in the literature:
You (1999): azimuthal angle of a reference vector about Q — constant for a given (hkl, UB); a crystal-orientation diagnostic. See
psi().Busing & Levy (1967): angle of sample rotation about Q relative to a reference orientation — the quantity physically varied in a ψ scan. See
psi_trajectory().
See Plan a Trajectory.
Serialization#
The complete diffractometer state — geometry, wavelength, lattice, reflections,
UB matrix, and all parameters — can be saved and restored via
to_dict() / from_dict() on AdHocDiffractometer.
The dict contains only JSON-compatible types; save to JSON (stdlib) or YAML
(pyyaml) without loss.