Surface Geometry and the Reference Vector#
This guide explains how to supply the surface normal n̂ and the azimuthal reference vector to modes that require them, and how to compute the resulting reference pseudo-angles (α_i, β_out, ψ, naz).
Background#
Several diffraction modes — fixed_psi_vertical, zaxis, reflectivity,
and others — require an external reference vector to complete their constraint.
In this package the reference vector is supplied as Miller indices (h, k, l),
not as a lab-frame Cartesian vector. The package converts to the lab frame
internally using the UB matrix.
Two separate reference vectors may be set:
surface_normal— the direction perpendicular to the sample surface, used byalpha_i,alpha_f,incidence_angle,exit_angle, and surface modes (zaxis,reflectivity,alpha_eq_beta_zaxis).azimuthal_reference— the direction used to define ψ = 0, used bypsi_angleandfixed_psi_*modes.
They may be the same vector (e.g. the surface normal is also the azimuthal reference) or different.
Set the surface normal#
import ad_hoc_diffractometer as ahd
g = ahd.presets.psic()
g.wavelength = 1.0 # Å
g.sample.lattice = ahd.Lattice(a=4.0, c=6.5)
ahd.ub_identity(g.sample)
# Surface normal for a (001)-cut sample: Miller indices (0, 0, 1)
g.surface_normal = (0, 0, 1)
print(g.surface_normal) # (0.0, 0.0, 1.0)
# Clear the surface normal
g.surface_normal = None
The setter accepts any three-element sequence of numbers and raises
ValueError for the zero vector or wrong shape.
Set the azimuthal reference#
# Azimuthal reference along the a-axis: (1, 0, 0)
g.azimuthal_reference = (1, 0, 0)
print(g.azimuthal_reference) # (1.0, 0.0, 0.0)
# Same vector as surface normal for a (001) surface
g.azimuthal_reference = (0, 0, 1)
Compute incidence and exit angles#
from ad_hoc_diffractometer import incidence_angle, exit_angle
g.surface_normal = (0, 0, 1)
g.mode_name = "bisecting_vertical"
solutions = g.forward(1, 0, 0)
for sol in solutions:
ai = incidence_angle(g, angles=sol)
af = exit_angle(g, angles=sol)
print(f"alpha_i = {ai:.4f}° beta_out = {af:.4f}°")
Both functions use the current stage angles when angles=None:
# Set current motor positions first
for name, value in solutions[0].items():
g.set_angle(name, value)
ai = incidence_angle(g) # uses current stage angles
Compute the azimuthal angle ψ#
from ad_hoc_diffractometer import psi_angle
g.azimuthal_reference = (0, 0, 1)
g.mode_name = "bisecting_vertical"
solutions = g.forward(1, 0, 0)
for sol in solutions:
psi = psi_angle(g, angles=sol)
print(f"psi = {psi:.4f}°")
ψ is the angle between the azimuthal reference vector (projected onto the plane perpendicular to Q) and the incident beam direction in that same plane. ψ = 0 when the reference vector lies in the scattering plane on the incident-beam side.
Compute naz#
from ad_hoc_diffractometer import naz_angle
g.surface_normal = (0, 0, 1)
naz = naz_angle(g) # uses current stage angles
print(f"naz = {naz:.4f}°")
naz is the azimuthal angle of the surface normal n̂ projected onto the horizontal plane of the lab frame.
Symmetric reflection condition (α_i = β_out)#
from ad_hoc_diffractometer import incidence_angle, exit_angle
g.surface_normal = (0, 0, 1)
g.mode_name = "bisecting_vertical"
solutions = g.forward(1, 0, 0)
for sol in solutions:
ai = incidence_angle(g, angles=sol)
af = exit_angle(g, angles=sol)
is_sym = abs(ai - af) < 0.01 # within 0.01°
print(f"alpha_i={ai:.3f}° beta_out={af:.3f}° symmetric={is_sym}")
Alternatively, use the built-in is_specular() method on the geometry:
for sol in solutions:
for name, value in sol.items():
g.set_angle(name, value)
print(f"specular: {g.is_specular()}")
Serialisation#
surface_normal and azimuthal_reference are serialised in to_dict()
and restored by from_dict():
import json
from ad_hoc_diffractometer import AdHocDiffractometer
g.surface_normal = (0, 0, 1)
g.azimuthal_reference = (1, 0, 0)
d = g.to_dict()
print(d["surface_normal"]) # [0.0, 0.0, 1.0]
print(d["azimuthal_reference"]) # [1.0, 0.0, 0.0]
g2 = AdHocDiffractometer.from_dict(d)
print(g2.surface_normal) # (0.0, 0.0, 1.0)
Reference constraint modes#
Modes that use a ReferenceConstraint require the appropriate reference
vector to be set on the geometry. fixed_psi_* modes require
azimuthal_reference; surface modes (zaxis, reflectivity) require
surface_normal.
g.azimuthal_reference = (0, 0, 1)
g.mode_name = "fixed_psi_vertical"
cs = g.modes["fixed_psi_vertical"]
rc = cs.reference_constraint
print(rc.has_reference_vector(g)) # True — vector is set
print(rc.is_implemented(g)) # True — solver available
# forward() returns bisecting solutions whose natural ψ matches the target
solutions = g.forward(1, 0, 0)
The fixed_psi solver acts as a validation filter: ψ is a pure
phi-frame quantity that is the same for every Bragg solution of a given
(h,k,l) and UB. The solver computes the natural ψ and returns solutions
only if it matches the stored target — otherwise it returns an empty list.
See also#
Work with Constraints and Diffraction Modes — constraint framework and run-time mode customisation