How to use the ad_hoc solver#
This guide shows how to create a diffractometer with the ad_hoc
solver, orient a crystalline sample, and compute reciprocal-space
positions. It assumes you have already installed the
package.
The ad_hoc solver wraps the ad_hoc_diffractometer library. This library
provides 10 diffractometer geometries ranging from Eulerian four-circle to
six-circle, kappa, and surface configurations. See ad_hoc solver for
the full list.
Create a diffractometer#
Four-circle vertical (the default geometry):
import hklpy2
fourc = hklpy2.creator(
solver="ad_hoc",
geometry="fourcv",
name="fourc",
)
The object fourc has four real axes (omega, chi, phi,
ttheta) and three pseudo axes (h, k, l).
Six-circle (psic):
psic = hklpy2.creator(
solver="ad_hoc",
geometry="psic",
name="psic",
)
Kappa geometry (set the kappa tilt angle via solver_kwargs):
kappa = hklpy2.creator(
solver="ad_hoc",
geometry="kappa4cv",
name="kappa",
solver_kwargs={"kappa_alpha_deg": 50},
)
Set the crystal lattice#
fourc.add_sample(name="silicon", a=hklpy2.SI_LATTICE_PARAMETER)
fourc.beam.wavelength.put(1.0)
Add orientation reflections#
Provide two reflections measured at known motor positions:
import math
theta = math.degrees(math.asin(1.0 / (2 * 5.431)))
tth = 2 * theta
r1 = fourc.add_reflection(
pseudos={"h": 1, "k": 0, "l": 0},
reals={"omega": theta, "chi": 0, "phi": 0, "ttheta": tth},
wavelength=1.0,
name="r1",
)
r2 = fourc.add_reflection(
pseudos={"h": 0, "k": 1, "l": 0},
reals={"omega": theta, "chi": 0, "phi": 90, "ttheta": tth},
wavelength=1.0,
name="r2",
)
Calculate the UB matrix#
fourc.core.calc_UB(r1, r2)
Choose an operating mode#
The default mode for fourcv is bisecting. To change it:
fourc.core.mode = "fixed_phi"
See ad_hoc solver for the full mode tables for each geometry.
Compute motor positions (forward)#
fourc.forward(1, 0, 0)
This returns a single chosen motor-position solution for the given
(h, k, l) (an Hklpy2DiffractometerRealPos). The underlying
solver’s forward() may return multiple solutions; the
diffractometer picks one according to the policy assigned to
fourc._forward_solution (defaults to
hklpy2.utils.pick_first_solution()). The complete list of
solutions can be returned from fourc.core.forward((1, 0, 0)).
See the upstream hklpy2 guide How to Choose the Default forward()
Solution
for details.
Tip
The two call shapes differ: fourc.forward(1, 0, 0) takes
h, k, l as separate positional arguments, while
fourc.core.forward((1, 0, 0)) takes a single sequence
(tuple / list / ndarray) or dict (e.g. {"h": 1, "k": 0, "l": 0}).
Compute (h, k, l) from motor positions (inverse)#
fourc.inverse(fourc.real_position)
This returns the (h, k, l) values computed from the supplied
motor positions. fourc.real_position is the current readout of
all real axes; pass a different set of values to compute (h, k, l)
at a hypothetical position instead.
Derived quantities (ψ, incidence, emergence, n_az, OMEGA)#
AdHocSolver exposes the six derived-quantity helpers from
ad_hoc_diffractometer.reference as methods, so users do not need
to reach into solver._geom:
Method |
Returns |
Geometry prerequisite |
|---|---|---|
|
Azimuthal angle ψ (deg) from motors |
|
|
Incidence angle (deg) |
|
|
Emergence angle (deg) |
|
|
Lab-frame azimuthal angle of n̂ (deg) |
|
|
SPEC |
none |
|
Natural ψ (deg) from UB; |
|
angles may be a dict keyed by real-axis name (any subset); None
(default) uses the geometry’s current angles. Unknown axis names raise
SolverError; a non-dict input raises
TypeError.
The reference vectors azimuth and surface_normal
are still configured on the underlying geometry object:
import hklpy2
psic2 = hklpy2.creator(name="psic2", geometry="psic", solver="ad_hoc")
solver = psic2.core.solver
solver._geom.azimuth = (0, 0, 1)
solver._geom.surface_normal = (1, 1, 6)
solver._geom.wavelength = 1.0
angles = dict(mu=0, eta=20, chi=30, phi=15, nu=0, delta=40)
solver.set_reals(angles)
psi = solver.psi_angle(angles)
incidence = solver.incidence_angle(angles)
emergence = solver.emergence_angle(angles)
naz = solver.naz_angle(angles)
omega = solver.omega_pseudo(angles)
natural = solver.natural_psi(1, 1, 1)
See the upstream ad_hoc_diffractometer.reference module for the mathematical definitions.
Set the reference vector (n̂)#
Modes that involve a surface normal or an azimuthal reference (for
example fixed_psi, fixed_incidence_vertical, zaxis,
reflectivity) require an external direction vector. In every
per-mode table that vector is shown as n̂ (rendered as the
n_hat key in the mode’s extras), but n̂ is a documentation
placeholder, not a settable input: the actual vector lives on the
underlying geometry object, on one of two attributes selected by the
mode’s reference constraint.
Mode reference constraint |
Geometry attribute |
Set with |
|---|---|---|
|
|
|
|
|
|
|
(none required) |
— |
To discover which attribute the active mode needs, ask the geometry directly:
psic2.core.mode = "fixed_incidence_vertical"
attr = psic2.core.solver._geom.required_reference_vector
# attr is 'surface_normal' for this mode; 'azimuth'
# for psi / naz modes; None when the active mode requires no
# reference vector.
Two ways to set the vector#
Through the extras dict — the n_hat extra routes to
whichever attribute the active mode requires
(surface_normal for surface modes, azimuth for psi / naz
modes), so it works for every reference-constraint mode:
psic2.core.extras = {"n_hat": (0, 0, 1)}
Directly on the geometry — required for azimuth;
also works for surface_normal:
psic2.core.solver._geom.surface_normal = (0, 0, 1)
psic2.core.solver._geom.azimuth = (1, 0, 0)
The argument is a length-3 sequence of Miller indices. (0, 0, 0)
is rejected with ValueError; the default is None (not set).
Clear an attribute by assigning None.
Caution
ad_hoc_diffractometer >= 0.11.1 emits a UserWarning when
cs.extras["n_hat"] is overwritten directly with a real value
(the assignment has no effect on forward()). Use one of the
two recipes above instead; both bypass the placeholder.
See the upstream Surface Geometry and the Reference Vector how-to for the full mathematical background.
Override a fixed-axis default value#
Each fixed_<axis> mode (for example fourcv fixed_chi,
psic fixed_chi_vertical, fixed_incidence_fixed_chi_fixed_phi)
carries a default scalar value baked into the geometry’s YAML
definition. Constraint values are immutable, so changing a default
replaces the underlying
ConstraintSet rather than mutating
it in place.
Use update_mode_constraints()
to override one or more defaults without touching solver internals.
Override a single sample-stage default:
psic2.core.solver.update_mode_constraints("fixed_chi_vertical", chi=45.0)
psic2.core.mode = "fixed_chi_vertical"
Override several stages at once on a multi-fix mode:
psic2.core.solver.update_mode_constraints(
"fixed_incidence_fixed_chi_fixed_phi",
chi=15.0, phi=30.0, incidence=5.0,
)
Operate on the currently active mode by omitting mode_name:
psic2.core.solver.mode = "fixed_chi_vertical"
psic2.core.solver.update_mode_constraints(chi=10.0)
(Assigning to psic2.core.solver.mode updates the adapter
synchronously, which the active-mode shortcut requires.
psic2.core.mode = ... is cached by hklpy2’s
Core and pushed to the solver on the next
update_solver(), so it is not seen by
update_mode_constraints until a forward() runs.)
Unknown mode names, unknown constraint names, and values rejected by
the underlying library all raise SolverError
with a descriptive message.
Persistent overrides vs. per-call reference scalars#
Two distinct routes touch reference-constraint scalars
(psi, incidence, emergence) on modes that expose them:
update_mode_constraints()is the persistent route — the new value becomes the mode’s default for every subsequentforward()call until overridden again.The
diff.core.extras = {"psi": ...}setter is the per-call route — values come from hklpy2’s Core and are pushed to the solver on the nextupdate_solver().
For sample-stage constraints (chi, phi, mu, eta …)
update_mode_constraints is the only route; those names are not
exposed through the extras interface.
See the upstream Work with Constraints and Diffraction Modes how-to for the full constraint-system background.
Available geometries at a glance#
Geometry |
Real axes |
Modes |
Default mode |
|---|---|---|---|
omega, chi, phi, ttheta |
6 |
bisecting |
|
omega, chi, phi, ttheta |
6 |
bisecting |
|
mu, eta, chi, phi, nu, delta |
24 |
bisecting_vertical |
|
alpha, omega, chi, phi, delta, gamma |
6 |
bisecting_4c |
|
mu, omega, chi, phi, ttheta |
5 |
bisecting_4c |
|
komega, kappa, kphi, ttheta |
7 |
bisecting |
|
komega, kappa, kphi, ttheta |
6 |
bisecting |
|
mu, komega, kappa, kphi, nu, delta |
14 |
bisecting_vertical |
|
alpha, Z, delta, gamma |
2 |
zaxis |
|
mu, Z, nu, delta |
2 |
fixed_mu |
Register a custom YAML geometry#
Since ad_hoc_diffractometer 0.10.0 (issue
#267),
geometries are described in declarative YAML files. You can extend the
ad_hoc solver with your own geometry by registering a YAML file
before creating the diffractometer. The
AdHocSolver discovers geometries
dynamically from the library’s registry, so no wrapper change is
required.
import ad_hoc_diffractometer as ahd
import hklpy2
# Register a YAML geometry from disk under the name 'mybeamline'.
ahd.register_geometry_file("/path/to/mybeamline.yml", name="mybeamline")
# Or load and inspect without registering:
geom = ahd.load_geometry_file("/path/to/mybeamline.yml")
# The new geometry is now discoverable through the ad_hoc solver.
diff = hklpy2.creator(
solver="ad_hoc",
geometry="mybeamline",
name="mybeamline",
)
The name argument is optional; when omitted, the geometry is
registered under the name: field declared inside the YAML file.
See the
ad_hoc_diffractometer schema
for the YAML format.
Third-party packages can alternatively contribute geometries via the
"ad_hoc_diffractometer.geometries" entry-point group; those are
discovered automatically the first time
ad_hoc_diffractometer.list_geometries() is called.
Persistence across export() / simulator_from_config()#
User-registered geometries and modifications to built-in
geometries (e.g. modes added at runtime) survive a save/restore
cycle when the diffractometer is reconstructed via
hklpy2.simulator_from_config() (see 108).
AdHocSolver._metadata writes a
geometry_state snapshot into the solver: block of the
YAML when the live geometry differs from a fresh reference, and
simulator_from_config() forwards it as a solver_kwargs
entry that AdHocSolver.__init__() replays via
ad_hoc_diffractometer.AdHocDiffractometer.from_dict():
# Suppose `diff` is an AdHoc-backed diffractometer with a
# custom mode added to its psic geometry at runtime. See
# /path/to/mybeamline.yml for the full registration example
# above; here we focus on the round-trip pattern.
diff.export("diff.yaml")
...
import hklpy2
diff2 = hklpy2.simulator_from_config("diff.yaml")
# The custom mode (and any other in-memory modifications to
# the geometry's modes table) is back in diff2's solver.
Vanilla built-in geometries with no modifications round-trip cleanly without any extra payload in the YAML.
Note
geometry_state carries the geometry structure (stages,
modes, basis, cut points, etc.) but omits the
samples, active_sample, and wavelength fields:
those are managed independently by hklpy2 and are restored
through the dedicated samples: / beam: blocks to
avoid double-restore.
hklpy2.diffract.DiffractometerBase.restore() does not
re-create the underlying solver, so
simulator_from_config() is the supported entry
point for full restoration. hklpy2.Core also caches the
active mode; call diffractometer.forward(...) (or
diffractometer.core.update_solver()) once after setting
core.mode before export() so the saved mode: field
reflects the current value.
See also
ad_hoc solver — full reference for all geometries and modes
How to benchmark a solver geometry — measure solver throughput
hklpy2 user guide — full hklpy2 documentation