How to Choose the Default forward() Solution#
The forward() method may return
multiple valid real-axis positions for a given set of pseudo-axis coordinates.
A solution picker function selects one solution from that list.
The four-stage forward pipeline
Computing a single motor position from pseudo-axis coordinates (e.g.
h, k, l) involves four distinct stages across three layers of
hklpy2. This guide covers Stage 4 — the solution picker.
![digraph forward_pipeline {
graph [rankdir=TB, splines=ortho, nodesep=0.6, ranksep=0.5,
fontname="sans-serif", bgcolor="transparent"]
node [shape=box, style="rounded,filled", fontname="sans-serif",
fontsize=11, margin="0.15,0.08"]
edge [fontname="sans-serif", fontsize=10]
pseudos [label="pseudos\n(h, k, l)", shape=ellipse,
fillcolor="#e8f4e8", color="#4a7c4a"]
s1 [label="Stage 1: SolverBase.forward()\nBackend engine returns ALL theoretical\nsolutions for the pseudos, geometry, mode.\nSPEC/diffcalc: geometry engine\nlibhkl: pseudo_axis_values_set()",
fillcolor="#dce8f8", color="#3a6898"]
s2 [label="Stage 2: apply_cut() [Core]\nEach axis angle is mapped into\n[cut_point, cut_point+360).\nControls representation, not validity.\nSPEC: cuts | diffcalc: _cut_angles()",
fillcolor="#fdf3dc", color="#a07820"]
s3 [label="Stage 3: LimitsConstraint.valid() [Core]\nSolutions whose wrapped axis values\nfall outside configured limits are discarded.\nSPEC: lm | diffcalc: is_position_within_limits()",
fillcolor="#fdf3dc", color="#a07820"]
s4 [label="Stage 4: solution picker [DiffractometerBase]\nOne solution is selected from survivors\nfor motor motion (ophyd PseudoPositioner).",
fillcolor="#f0e8f8", color="#6a3a98",
style="rounded,filled,bold"]
result [label="single real-axis position\nfor motor motion",
shape=ellipse, fillcolor="#e8f4e8", color="#4a7c4a"]
pseudos -> s1 [label="all theoretical solutions"]
s1 -> s2 [label="wrapped solutions"]
s2 -> s3 [label="filtered solutions"]
s3 -> s4 [label="one solution"]
s4 -> result
}](../_images/graphviz-41638728f38da2b650810f2c243dce112adaa3b8.png)
Four-stage forward() pipeline in hklpy2 (equivalent stages in SPEC and diffcalc shown in parentheses).#
The number of solutions entering Stage 4 depends on the backend Solver’s capabilities. Some engines analytically enumerate all mathematically valid solutions; others return only one. A single-element list is valid. See also forward() Contract.
This guide explains the built-in pickers, how to switch between them, and how to write a custom one.
Built-in solution pickers#
Two pickers are provided:
Function |
Behavior |
|---|---|
|
Returns the first solution in the list as supplied by the solver. |
|
Returns the solution whose real-axis values are closest (RMS distance) to the current motor positions. |
The default is pick_first_solution().
Check the current picker#
>>> e4cv._forward_solution
<function pick_first_solution at 0x...>
Switch to pick_closest_solution at runtime#
Assign directly to _forward_solution:
>>> from hklpy2.misc import pick_closest_solution
>>> e4cv._forward_solution = pick_closest_solution
Switch back:
>>> from hklpy2.misc import pick_first_solution
>>> e4cv._forward_solution = pick_first_solution
Set the picker at creation time#
Pass forward_solution_function as a dotted name string to
creator():
>>> import hklpy2
>>> e4cv = hklpy2.creator(
... name="e4cv",
... forward_solution_function="hklpy2.misc.pick_closest_solution",
... )
Or pass a callable directly to
__init__ when subclassing:
>>> from hklpy2.misc import pick_closest_solution
>>> e4cv = MyDiffractometerClass(
... "",
... name="e4cv",
... forward_solution_function=pick_closest_solution,
... )
Write a custom picker#
A picker function must accept two arguments and return one solution:
from typing import NamedTuple
def my_picker(position: NamedTuple, solutions: list[NamedTuple]) -> NamedTuple:
"""Return the solution with the smallest omega value."""
from hklpy2.misc import NoForwardSolutions
if not solutions:
raise NoForwardSolutions("No solutions.")
return min(solutions, key=lambda s: abs(s.omega))
Assign it at runtime or at creation:
>>> e4cv._forward_solution = my_picker
>>> e4cv = hklpy2.creator(
... name="e4cv",
... forward_solution_function="mymodule.my_picker",
... )
The picker interface#
All pickers must follow this interface:
positionnamed tupleThe current real-axis position of the diffractometer (e.g.
RealPosition(omega=..., chi=..., phi=..., tth=...)).solutionslist of named tuplesAll valid solutions returned by
forward(). Each element is a named tuple with the same fields asposition.
Returns: one named tuple from solutions.
Raises: NoForwardSolutions if solutions is empty.
See also
pick_first_solution(),
pick_closest_solution() — built-in pickers.
_forward_solution — the
attribute that holds the active picker.
forward() — returns all solutions before the
picker is applied.