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.
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.
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 lab frame.
See Orient a Crystal, Define the Sample Lattice, Case Study: Coordinate Convention and UB Matrix, and
Lattice.
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.presets.fourcv()
g.mode_name = "bisecting"
# Six-circle psic uses named variants
g = ahd.presets.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, polarisation 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.presets.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 from the convention via
(see kappa_axis_from_eulerian()).
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.
Modes accept virtual angle names#
Kappa modes accept the virtual angle names directly in
SampleConstraint:
from ad_hoc_diffractometer import ConstraintSet, SampleConstraint
g = ahd.presets.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.