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
}

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

pick_first_solution() (default)

Returns the first solution in the list as supplied by the solver.

pick_closest_solution()

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 tuple

The current real-axis position of the diffractometer (e.g. RealPosition(omega=..., chi=..., phi=..., tth=...)).

solutionslist of named tuples

All valid solutions returned by forward(). Each element is a named tuple with the same fields as position.

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.