orientation#
Import: ad_hoc_diffractometer.orientation
Functions#
- angles_to_phi_vector(geometry, **motor_angles)
Convert a set of motor angles to the scattering vector expressed in the phi-axis (innermost sample-stage) frame. This is the foundational computation needed for U and UB matrix determination.
- ub_from_one_reflection(sample, reflection, reference_hkl, reference_stage)
Compute a provisional U and UB from one reflection using the Rodrigues rotation that takes the crystal direction
B @ reference_hklto the lab direction given byreference_stage. Setssample.Uandsample.UBin-place; returns UB.- ub_from_two_reflections_bl1967(sample, r1, r2)
Compute U and UB from two orienting reflections using the Busing & Levy (1967) algorithm (eqs. 23-27). Sets
sample.Uandsample.UBin-place; returns UB.- ub_from_three_reflections_bl1967(sample, r1, r2, r3)
Compute UB directly from three reflections using the Busing & Levy (1967) direct method (eqs. 29-31), without prior knowledge of the lattice. Sets
sample.UBin-place; also setssample.Uif a lattice B is available. Returns UB.- ub_identity(sample)
Set U = I, UB = B; return UB. The crudest assumption.
- Future functions (separate issues):
ub_from_three_reflections_bl1967 — Busing & Levy 1967, eqs. 29-31 (#6)
References
Busing & Levy, Acta Cryst. 22, 457-464 (1967)
You, J. Appl. Cryst. 32, 614-623 (1999)
Functions#
|
Compute Q_phi from angle values without mutating any geometry state. |
|
Compute Q_phi using cached rotation matrices for fixed stages. |
|
Build a right-handed orthonormal 3×3 matrix from two linearly independent |
|
Convert a set of motor angles to the scattering vector in the phi frame. |
|
Compute a provisional U and UB from one reflection (Rodrigues method). |
|
Compute UB directly from three reflections (Busing & Levy 1967, eqs. 29-31). |
|
Compute U and UB from two orienting reflections (Busing & Levy 1967, eqs. 23-27). |
|
Set U so the crystal-Cartesian frame aligns with the physical |
Module Contents#
- ad_hoc_diffractometer.orientation._compute_q_phi(sample_stages: list, detector_stages: list, angles: dict[str, float], two_pi_over_lambda: float, y_eff: ndarray) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation._compute_q_phiCompute Q_phi from angle values without mutating any geometry state.
This is the inner hot-path function called hundreds of times per forward() invocation. It avoids all attribute mutation, dict copying, and save/restore overhead.
- Parameters:
sample_stages (list of Stage) – Sample stages in stacking order (floor-most first).
detector_stages (list of Stage) – Detector stages in stacking order (floor-most first).
angles (dict[str, float]) – Motor angles in degrees, keyed by stage name. Stages not present in the dict use the stage’s current
angleattribute.two_pi_over_lambda (float) – Pre-computed
2 * pi / wavelength.y_eff (numpy.ndarray, shape (3,)) – Pre-computed effective beam direction
R_inc.T @ y_hat.
- Returns:
Q_phi – Scattering vector in the phi frame, in Å⁻¹.
- Return type:
numpy.ndarray, shape (3,)
- ad_hoc_diffractometer.orientation._compute_q_phi_cached(sample_stages: list, detector_stages: list, angles: dict[str, float], two_pi_over_lambda: float, y_eff: ndarray, cached_Z_prefix: ndarray | None, free_sample_indices: list[int] | None, cached_D: ndarray | None) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation._compute_q_phi_cachedCompute Q_phi using cached rotation matrices for fixed stages.
When some stages have fixed angles across all Newton iterations, their rotation matrices are constant and can be pre-computed. This function uses the cached prefix product for the fixed portion and only computes rotation matrices for the free stages.
- Parameters:
sample_stages (list of Stage) – All sample stages in stacking order.
detector_stages (list of Stage) – All detector stages in stacking order.
two_pi_over_lambda (float)
y_eff (numpy.ndarray, shape (3,))
cached_Z_prefix (numpy.ndarray or None) – Pre-computed product of rotation matrices for all sample stages before the first free stage. None means no caching (compute all).
free_sample_indices (list of int or None) – Indices into sample_stages of the free (varying) stages. None means all stages are free (no caching).
cached_D (numpy.ndarray or None) – Pre-computed detector rotation matrix. None means compute it.
- Returns:
Q_phi
- Return type:
numpy.ndarray, shape (3,)
- ad_hoc_diffractometer.orientation._gram_schmidt_triple(v1: ndarray, v2: ndarray) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation._gram_schmidt_tripleBuild a right-handed orthonormal 3×3 matrix from two linearly independent vectors using Gram-Schmidt orthogonalisation.
The columns of the returned matrix
Tare:t1 = v1 / |v1| t3 = t1 × v2 / |t1 × v2| t2 = t3 × t1
so that
t1 ∥ v1,t2lies in the plane ofv1andv2, andt3is perpendicular to that plane. The triple(t1, t2, t3)is right-handed and orthonormal.- Parameters:
v1 (numpy.ndarray, shape (3,)) – Primary vector (must be non-zero).
v2 (numpy.ndarray, shape (3,)) – Secondary vector (must not be parallel to
v1).
- Returns:
T – Columns are
[t1, t2, t3], forming a right-handed orthonormal basis.- Return type:
numpy.ndarray, shape (3, 3)
- Raises:
ValueError – If
v1is the zero vector orv1andv2are parallel (cross product is zero).
Notes
This is the Gram-Schmidt construction used in Busing & Levy (1967) to build the orthonormal triples
Tc(crystal frame) andTφ(phi frame) for the two-reflection orientation algorithm (eqs. 23-27).
- ad_hoc_diffractometer.orientation.angles_to_phi_vector(geometry, **motor_angles: float) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation.angles_to_phi_vectorConvert a set of motor angles to the scattering vector in the phi frame.
The “phi frame” is the coordinate system seen from the innermost sample stage — the frame in which crystal reflections are expressed when computing the orientation (U) matrix.
Algorithm (Busing & Levy 1967, section “The phi-axis frame”):
Compute the total sample rotation matrix
Zas the product of all sample stage rotation matrices in outermost-leftmost order (BL1967 standard convention):Z = R_0 @ R_1 @ ... @ R_{n-1}
where
R_0is the floor-most (outermost) stage andR_{n-1}is the innermost stage closest to the sample. Z then maps phi-frame vectors to lab-frame vectors:v_lab = Z @ v_phi. Stages not supplied keep their currentangleattribute.Compute the total detector rotation matrix
Din the same outermost-leftmost order.The incident-beam unit vector in the lab frame is
ŷ(longitudinal direction,geometry.basis["longitudinal"]).The scattered-beam unit vector in the lab frame is
D @ ŷ.The scattering vector in the lab frame is:
Q_lab = (2π / λ) * (D @ ŷ - ŷ)
Rotate Q_lab back through the sample stack:
Q_phi = Z⁻¹ @ Q_lab = Zᵀ @ Q_lab
(Z is orthogonal, so Z⁻¹ = Zᵀ.)
- Parameters:
geometry (AdHocDiffractometer) – The diffractometer geometry. Must have
wavelengthset (not None).**motor_angles (float) – Motor angles in degrees, keyed by stage name. All stages present in the geometry may be supplied; stages not supplied keep their current
angleattribute. Only sample and detector stages affect the result; other stages (if any) are ignored.
- Returns:
Q_phi – Scattering vector in the phi frame, in units of Å⁻¹.
- Return type:
numpy.ndarray, shape (3,)
- Raises:
KeyError – If a supplied stage name does not exist in the geometry.
ValueError – If
geometry.wavelengthis None.ValueError – If the geometry has no sample stages.
ValueError – If the geometry has no detector stages.
Notes
The function is stateless: it does not modify the geometry’s stage angles. It computes rotation matrices directly from the supplied
motor_anglesvalues, so it is safe to call from multiple threads on the same geometry instance.The scattering vector Q_phi is independent of which sample stage is designated the “phi” axis; it is expressed in the frame of the last sample stage in the stacking order (the one closest to the sample).
Examples
>>> import ad_hoc_diffractometer as ahd >>> g = ahd.psic() >>> g.wavelength = 1.5406 # Cu Kα in Å >>> Q_phi = ahd.angles_to_phi_vector( ... g, ... mu=0, eta=20.97, chi=90, phi=0, nu=0, delta=41.94, ... ) >>> Q_phi # scattering vector for sapphire (006) in phi frame array([...])
References
Busing & Levy, Acta Cryst. 22, 457-464 (1967) — phi-axis frame You, J. Appl. Cryst. 32, 614-623 (1999) — psic geometry conventions
- ad_hoc_diffractometer.orientation.ub_from_one_reflection(sample, reflection, reference_hkl: tuple[float, float, float] = (0.0, 0.0, 1.0), reference_stage=None) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation.ub_from_one_reflectionCompute a provisional U and UB from one reflection (Rodrigues method).
A common first step in a diffractometer alignment session: assume that a known high-symmetry crystal direction (
reference_hkl) is parallel to a specific diffractometer axis (reference_stage). This gives a “fake” UB sufficient to predict where to scan for a second reflection.The algorithm:
Compute the crystal-frame direction:
Bh = B @ reference_hkl(normalized toBh_hat).Extract the lab-frame direction from
reference_stage(normalized tor_hat).Find the minimal rotation (Rodrigues) that takes
Bh_hattor_hat:axis = cross(Bh_hat, r_hat)angle = arccos(clip(dot(Bh_hat, r_hat), -1, 1))U = rotation_matrix(axis, degrees(angle))UB = U @ BStore
sample.U = Uandsample.UB = UB; returnUB.
Edge cases:
Parallel (
angle ≈ 0):Bh_hatalready points alongr_hat;U = I.Anti-parallel (
angle ≈ π): choose an arbitrary perpendicular axis (the first vector from[XHAT, YHAT, ZHAT]not parallel toBh_hat); rotate 180° about it.
Note
The result is approximate. If
reference_hklis not truly parallel toreference_stage.axis(e.g. χ = 89.32° rather than 90.00°), predicted angles for subsequent reflections will be slightly wrong. Refine withub_from_two_reflections_bl1967()once a second reflection is measured.- Parameters:
sample (Sample) – The sample whose
UandUBattributes are updated in-place.sample.latticemust be set. Ifsample.parentis set, it is used to resolve a stringreference_stage.reflection (Reflection or str) – A
Reflectionobject or the name of a reflection insample.reflections.reference_hkl (tuple of float, optional) – Miller indices of the crystal direction assumed to be aligned with
reference_stage. Default(0, 0, 1)(c-axis).reference_stage (Stage, str, or None, optional) –
The diffractometer axis assumed to be parallel to
reference_hkl.Stageobject:stage.axisis used directly (recommended; the sign convention is already encoded in the Stage).str: looked up assample.parent.stage(name).Noneandsample.parentis set: defaults tosample.parent.stage("phi").Noneandsample.parentisNone: raisesValueError.
- Returns:
UB – Sets
sample.Uandsample.UBin-place before returning.- Return type:
numpy.ndarray, shape (3, 3)
- Raises:
KeyError – If
reflectionis a string not found insample.reflections.ValueError – If
reference_stagecannot be resolved (no parent, no stage).ValueError – If
reference_hklmaps to the zero vector under B.
Examples
>>> g = psic() >>> g.add_sample("sapphire", Lattice(a=4.758, c=12.991)) >>> g.sample = "sapphire" >>> g.add_reflection("r1", hkl=(0, 0, 6), ... angles={"mu": 0, "eta": 20.97, "chi": 90, ... "phi": 0, "nu": 0, "delta": 41.94}) >>> g.sample.reflections.setor0("r1") >>> UB = ub_from_one_reflection( ... g.sample, "r1", ... reference_hkl=(0, 0, 1), ... reference_stage=g.stage("phi"), ... )
- ad_hoc_diffractometer.orientation.ub_from_three_reflections_bl1967(sample, r1, r2, r3) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation.ub_from_three_reflections_bl1967Compute UB directly from three reflections (Busing & Levy 1967, eqs. 29-31).
This method requires no prior knowledge of the lattice: it computes UB directly by matrix inversion from three measured reflections. If a lattice B matrix is available on the sample, U is also derived.
Algorithm (BL1967 eqs. 28-31):
For each reflection i compute the phi-frame scattering vector
hiφ = angles_to_phi_vector(geometry, **ri.angles)(eq. 28 gives the magnitude;angles_to_phi_vectoralready carries the full vector in Å⁻¹).Stack as column matrices:
Hφ = [h1φ | h2φ | h3φ] (3×3, columns are phi-frame vectors) H = [h1 | h2 | h3 ] (3×3, columns are Miller-index triples)
UB = Hφ @ inv(H)(eq. 31).If
sample.latticeis set:U = UB @ inv(B)(derived from UB).Store
sample.UB = UB(first) andsample.U = U; returnUB.
- Parameters:
sample (Sample) – The sample whose
UB(andU) attributes are updated in-place.sample.parentmust be a geometry withwavelengthset.r1 (Reflection or str) – Three orienting reflections. Each may be a
Reflectionobject or the name of a reflection insample.reflections.r2 (Reflection or str) – Three orienting reflections. Each may be a
Reflectionobject or the name of a reflection insample.reflections.r3 (Reflection or str) – Three orienting reflections. Each may be a
Reflectionobject or the name of a reflection insample.reflections.
- Returns:
UB –
sample.UBis set first (directly, via eq. 31), thensample.U = UB @ inv(B)is derived. Both are set in-place.- Return type:
numpy.ndarray, shape (3, 3)
- Raises:
KeyError – If any of
r1,r2,r3is a string not found insample.reflections.TypeError – If any argument is not a
Reflectionor a string.ValueError – If
sample.parentisNone.ValueError – If the three Miller-index column matrix
His singular (|det(H)| < tol), i.e. the three hkl vectors are coplanar.ValueError – If
sample.parent.wavelengthisNone.
- Warns:
UserWarning – If
det(H) < 0, the hkl triples form a left-handed system. The computation proceeds but the sign convention may give U withdet(U) = -1; consider swapping r1 and r2 to make det(H) > 0.
Notes
UB is computed first (
sample.UB = Hφ @ H⁻¹). U is then derived assample.U = UB @ B⁻¹. This is the opposite order fromub_from_two_reflections_bl1967, where U is computed first.The method does not require a known lattice:
His formed from the raw hkl indices, not fromB @ hkl. However, ifsample.latticeis the package default (cubic, a = 1 Å) rather than a measured lattice, the derived U will not be physically meaningful.If
det(H)is exactly zero (degenerate reflections),numpy.linalg.invwill raiseLinAlgError, which is caught and re-raised asValueError.Examples
>>> import ad_hoc_diffractometer as ahd >>> import math >>> g = ahd.psic() >>> g.wavelength = 2 * math.pi >>> g.sample.lattice = ahd.Lattice(a=2 * math.pi) >>> g.add_reflection("r1", hkl=(1, 0, 0), ... angles={"mu": 0, "eta": 30, "chi": 0, "phi": 0, "nu": 0, "delta": 60}) >>> g.add_reflection("r2", hkl=(0, 1, 0), ... angles={"mu": 0, "eta": 30, "chi": 0, "phi": 90, "nu": 0, "delta": 60}) >>> g.add_reflection("r3", hkl=(0, 0, 1), ... angles={"mu": 0, "eta": 30, "chi": 90, "phi": 30, "nu": 0, "delta": 60}) >>> UB = ahd.ub_from_three_reflections_bl1967(g.sample, "r1", "r2", "r3")
References
Busing & Levy, Acta Cryst. 22, 457-464 (1967), eqs. 28-31.
- ad_hoc_diffractometer.orientation.ub_from_two_reflections_bl1967(sample, r1=None, r2=None) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation.ub_from_two_reflections_bl1967Compute U and UB from two orienting reflections (Busing & Levy 1967, eqs. 23-27).
Given two reflections with known hkl and measured motor angles, and a known lattice (B matrix), this function computes the orientation matrix U and then UB = U @ B, storing both on the sample.
Algorithm (BL1967 eqs. 23-27):
For each reflection, call
angles_to_phi_vector()to get the scattering vector in the phi frame:u1φ,u2φ.From hkl and the lattice B matrix:
h1c = B @ h1,h2c = B @ h2.Build orthonormal triple
Tcin the crystal frame via Gram-Schmidt:t1c ∥ h1c,t2cin the plane ofh1candh2c,t3c = t1c × t2c.Build the matching triple
Tφin the phi frame fromu1φ,u2φ.Compute
U = Tφ @ Tc.T(eq. 27; Tc is orthogonal so Tc⁻¹ = Tc.T).Compute
UB = U @ B.Store
sample.U = U,sample.UB = UB; returnUB.
- Parameters:
sample (Sample) – The sample whose
UandUBattributes are updated in-place.sample.latticemust be set.sample.parentmust be a geometry withwavelengthset (it is used to callangles_to_phi_vector).r1 (Reflection, str, or None) – Primary orienting reflection. If
None, defaults tosample.reflections.orienting_reflections[0].r2 (Reflection, str, or None) – Secondary orienting reflection. If
None, defaults tosample.reflections.orienting_reflections[1].
- Returns:
UB – Sets
sample.U(first) andsample.UB = sample.U @ Bin-place before returning.- Return type:
numpy.ndarray, shape (3, 3)
- Raises:
KeyError – If
r1orr2is a string not found insample.reflections.ValueError – If
r1orr2isNoneand the required orienting reflection has not been designated (setor0/setor1not called).ValueError – If
sample.parentisNone(needed to callangles_to_phi_vector).ValueError – If
sample.parent.wavelengthisNone.ValueError – If the two reflections are parallel in the crystal frame (h1c and h2c collinear) or in the phi frame (u1φ and u2φ collinear).
TypeError – If
r1orr2is not aReflection, string, orNone.
Notes
U is computed first (
sample.U = Tφ @ Tc.T), then UB is derived from it (sample.UB = sample.U @ B).The wavelength used for
angles_to_phi_vectoris taken fromsample.parent.wavelength. If a reflection carries its ownwavelengthattribute, that is not used here; the geometry’s wavelength governs the conversion from motor angles to Q_phi.References
Busing & Levy, Acta Cryst. 22, 457-464 (1967), eqs. 23-27.
Examples
>>> import ad_hoc_diffractometer as ahd >>> g = ahd.psic() >>> g.wavelength = 1.5406 >>> g.add_sample("sapphire", ahd.Lattice(a=4.758, c=12.991)) >>> g.sample = "sapphire" >>> g.add_reflection("r1", hkl=(0, 0, 6), ... angles={"mu": 0, "eta": 20.97, "chi": 90, "phi": 0, ... "nu": 0, "delta": 41.94}) >>> g.add_reflection("r2", hkl=(1, 0, 4), ... angles={"mu": 0, "eta": 23.72, "chi": 57.04, "phi": 0, ... "nu": 0, "delta": 48.13}) >>> g.sample.reflections.setor0("r1") >>> g.sample.reflections.setor1("r2") >>> UB = ahd.ub_from_two_reflections_bl1967(g.sample)
- ad_hoc_diffractometer.orientation.ub_identity(sample) ndarray[source]#
Import:
ad_hoc_diffractometer.orientation.ub_identitySet U so the crystal-Cartesian frame aligns with the physical (longitudinal, vertical, transverse) triple, and UB = U @ B.
The default UB placeholder used before a real UB is set by either reflection-based computation (
ub_from_one_reflection(),ub_from_two_reflections_bl1967(),ub_from_three_reflections_bl1967()) or direct assignment (sample.UB = ...orsample.U = ...).Definition (issue #280)#
The columns of U are the geometry’s physical-direction unit vectors expressed in basis-Cartesian coordinates:
U[:, 0] = +longitudinal (the beam direction) U[:, 1] = +vertical U[:, 2] = +transverse
Under the Busing & Levy 1967 crystal-Cartesian convention (a* along crystal-x, b* in the crystal-x-y plane, c* completing the right-handed triple), this places:
the crystal a* axis physically along the beam,
the crystal-y axis physically along vertical,
the crystal-z axis physically along transverse.
This matches the
hkl_soleilconvention (U = identityaligns a* along the beam +x; see https://people.debian.org/~picca/hkl/hkl.html).Why this is basis-independent#
The geometry’s basis assigns Cartesian unit vectors to the three physical-direction labels. The columns of U are taken directly from those physical-direction vectors, so the resulting physical crystal orientation does not depend on which Cartesian direction the basis labels assign to (vertical, longitudinal, transverse). Two geometries representing the same physical equipment under different basis labelings (e.g.
BASIS_BLvsBASIS_YOU) produce numerically different U matrices but identical physical Bragg orientations — and therefore identicalforward()results.The pre-#280 implementation set
U = numpy.eye(3)in basis coordinates, which made the physical crystal orientation basis-dependent: underBASIS_BLthe crystal a-axis ended up physically along transverse, but underBASIS_YOUthe same code put it along vertical, breaking basis invariance offorward()reachability and|2θ|for the same equipment.Why this is inclination-independent#
U is derived from the geometry’s declared basis vectors (a property of the YAML, set once), not from the inclination-tilted effective beam
y_eff = R_inc.T @ y_hat. Tilting the entire diffractometer relative to the beamline (issue #15inclination_matrix) does not silently re-orient the crystal: the crystal is mounted on the diffractometer, not on the beamline, so its reference orientation lives in the diffractometer’s intrinsic frame.- param sample:
The sample whose
UandUBattributes are updated in-place.sample.parentmust be set (the parentAdHocDiffractometerprovides the basis vectors).- type sample:
Sample
- returns:
UB –
UB = U @ B. Also stored on the sample assample.UB.- rtype:
numpy.ndarray, shape (3, 3)
- raises ValueError:
If
sample.parentisNone(cannot resolve the geometry’s basis without a parent).
References
Busing & Levy, Acta Cryst. 22, 457-464 (1967) — the crystal-Cartesian convention used in the B matrix.
Issue #280 — the rotation-composition-order audit that exposed the pre-existing basis bias of the old
U = Iplaceholder.hkl_soleil documentation — the published external convention this implementation matches.