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 = I (identity) and UB = B; return UB. |
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
Z(product of all sample stage rotation matrices, floor-most first) from the supplied motor angles (stages not supplied keep their currentangleattribute).Compute the total detector rotation matrix
D.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 = I (identity) and UB = B; return UB.
The crudest orientation assumption: crystal axes are aligned with the lab axes. Useful as a starting point when no reflection information is available at all.
- Parameters:
sample (Sample) – The sample whose
UandUBattributes are updated in-place.- Returns:
UB
- Return type:
numpy.ndarray, shape (3, 3)