Source code for hkl.engine

"""
Low-level support to Hkl computation engine support for diffractometers

.. autosummary::

    ~CalcParameter
    ~Engine
    ~Parameter
    ~Solution

"""

from __future__ import print_function

import logging
from collections import OrderedDict

from . import util

__all__ = """
    CalcParameter
    Engine
    Parameter
    Solution
""".split()
logger = logging.getLogger(__name__)


AXES_READ = 0
AXES_WRITTEN = 1


[docs] class Parameter(object): """HKL library parameter object .. autosummary:: ~inverted ~hkl_parameter ~units ~name ~value ~user_units ~default_units ~limits Example:: Parameter( name='None (internally: ux)', limits=(min=-180.0, max=180.0), value=0.0, fit=True, inverted=False, units='Degree') """ def __init__(self, param, units="user", name=None, inverted=False): self._param = param self._unit_name = units self._units = util.units[units] self._name = name self._inverted = inverted @property def inverted(self): """Is the value inverted internally?""" return self._inverted @property def hkl_parameter(self): """The HKL library parameter object""" return self._param @property def units(self): """Either ``user`` or ``default``.""" return self._unit_name @property def name(self): """Name of this parameter.""" name = self._param.name_get() if self._name != name: return f"{self._name} (internally: {name})" return name @property def value(self): """ Value used (in :meth:`~hkl.calc.CalcRecip.forward()` method) if held constant by the :attr:`~hkl.engine.Engine.mode`. """ return self._param.value_get(self._units) @value.setter def value(self, value): if self._inverted: value *= -1.0 self._param.value_set(value, self._units) @property def user_units(self): """A string representing the user unit type""" return self._param.user_unit_get() @property def default_units(self): """A string representing the default unit type""" return self._param.default_unit_get() @property def fit(self): """True if the parameter can be fit or not""" return bool(self._param.fit_get()) @fit.setter def fit(self, fit): self._param.fit_set(int(fit)) @property def limits(self): """(low, high)""" if self._inverted: low, high = self._param.min_max_get(self._units) return (-high, -low) else: return self._param.min_max_get(self._units) @limits.setter def limits(self, lims): low, high = lims if self._inverted: self._param.min_max_set(-high, -low, self._units) else: self._param.min_max_set(low, high, self._units) def _repr_info(self): r = [ f"name={self.name!r}", f"limits={self.limits!r}", f"value={self.value!r}", f"fit={self.fit!r}", f"inverted={self.inverted!r}", ] if self._unit_name == "user": r.append(f"units={self.user_units!r}") else: r.append(f"units={self.default_units!r}") return r def __repr__(self): return f"{self.__class__.__name__}({', '.join(self._repr_info())})" def __str__(self): info = self._repr_info() # info.append(self.) return f"{self.__class__.__name__}({', '.join(info)})"
[docs] class Solution(object): """ Solution of the conversion from (hkl) to axes. .. autosummary:: ~axis_names ~positions ~select ~units """ def __init__(self, engine, list_item, class_): self._class = class_ self._list_item = list_item.copy() self._geometry = list_item.geometry_get().copy() self._engine = engine def __getitem__(self, axis): return self._geometry.axis_get(axis) @property def axis_names(self): """List real axis names of the solution.""" return self._geometry.axis_names_get() @property def positions(self): """List real axis values of the solution.""" return self._class(*self._geometry.axis_values_get(self._engine._units)) @property def units(self): """ """ return self._engine.units
[docs] def select(self): """Select a solution.""" self._engine._engine_list.select_solution(self._list_item)
def _repr_info(self): r = [ repr(self.positions), f"units={self._engine.units!r}", ] return r def __repr__(self): return f"{self.__class__.__name__}({', '.join(self._repr_info())})"
[docs] class Engine(object): """ HKL calculation engine .. autosummary:: ~engine ~mode ~name ~parameters ~parameters_values ~pseudo_axes ~pseudo_axis_names ~pseudo_positions ~solutions ~units ~update """ def __init__(self, calc, engine, engine_list): self._calc = calc self._engine = engine self._engine_list = engine_list @property def name(self): """Name of this engine.""" return self._engine.name_get() @property def axes_c(self): """HKL real axis names (held constant during forward() computation).""" return [axis for axis in self.axes_r if axis not in self.axes_w] @property def axes_r(self): """HKL real axis names (read-only).""" return self._engine.axis_names_get(AXES_READ) @property def axes_w(self): """HKL real axis names (written by forward() computation).""" return self._engine.axis_names_get(AXES_WRITTEN) @property def mode(self): """HKL calculation mode (see also `HklCalc.modes`)""" return self._engine.current_mode_get() @mode.setter def mode(self, mode): if mode not in self.modes: raise ValueError("Unrecognized mode %r; choose from: %s" % (mode, ", ".join(self.modes))) return self._engine.current_mode_set(mode) @property def modes(self): return self._engine.modes_names_get() @property def solutions(self): """Allowed real solutions given the pseudo position.""" return tuple(self._solutions)
[docs] def update(self): """Calculate the pseudo axis positions from the real axis positions.""" # TODO: though this works, maybe it could be named better on the hkl # side? either the 'get' function name or the fact that the EngineList # is more than just a list... self._engine_list.get()
@property def parameters(self): """List the names of the additional parameters.""" return self._engine.parameters_names_get() @property def parameters_values(self): """List the values of the additional parameters.""" return self._engine.parameters_values_get(self._units) @property def pseudo_axis_names(self): """List the names of the pseudo axes.""" return self._engine.pseudo_axis_names_get() @property def pseudo_axes(self): """List of pseudo axes as tuples: ``[(name, value)]``.""" return OrderedDict(zip(self.pseudo_axis_names, self.pseudo_positions)) @property def pseudo_positions(self): """List the values of the pseudo positioners.""" return self._engine.pseudo_axis_values_get(self._units) @pseudo_positions.setter def pseudo_positions(self, values): try: geometry_list = self._engine.pseudo_axis_values_set(values, self._units) except util.GLib.GError as ex: raise ValueError("Calculation failed (%s)" % ex) Position = self._calc.Position def get_position(item): return Position(*item.geometry_get().axis_values_get(self._units)) self._solutions = [get_position(item) for item in geometry_list.items()] def __getitem__(self, name): try: return self.pseudo_axes[name] except KeyError: raise ValueError("Unknown axis name: %s" % name) def __setitem__(self, name, value): values = self.pseudo_positions try: idx = self.pseudo_axis_names.index(name) except IndexError: raise ValueError("Unknown axis name: %s" % name) values[idx] = float(value) self.pseudo_positions = values @property def units(self): """The units used for calculations""" return self._calc.units @property def _units(self): """The (internal) units used for calculations""" return self._calc._units @property def engine(self): """The calculation engine""" return self._engine def _repr_info(self): r = [ f"parameters={self.parameters!r}", f"pseudo_axes={self.pseudo_axes!r}", f"mode={self.mode!r}", f"modes={self.modes!r}", f"units={self.units!r}", ] return r def __repr__(self): return f"{self.__class__.__name__}({', '.join(self._repr_info())})"
# when updating parameters we need to update the parent geometry object
[docs] class CalcParameter(Parameter): """ Like calc parameter but needs reference to a geometry object. Updates to the parameter should be propagated back to the geometry. .. autosummary:: ~fit ~limits ~value Parameters ---------- param : HklParameter geometry: Geometry object """ def __init__(self, param, geometry, *args, **kwargs): super().__init__(param, *args, **kwargs) self.param_name = param.name_get() # you may wonder why we stash geometry object rather than axis # this is because the axis params can only be updated from the geometry self._geometry = geometry @property def limits(self): """Allowed range of values (low, high).""" axis = self._geometry.axis_get(self.param_name) low, high = axis.min_max_get(self._units) if self._inverted: return (-high, -low) return low, high @limits.setter def limits(self, lims): low, high = lims if self._inverted: low, high = -high, -low self._param.min_max_set(low, high, self._units) # param name is the true name of axis axis = self._geometry.axis_get(self.param_name) axis.min_max_set(low, high, self._units) self._geometry.axis_set(self.param_name, axis) @property def value(self): """Position of this axis.""" axis = self._geometry.axis_get(self.param_name) return axis.value_get(self._units) @value.setter def value(self, value): if self._inverted: value *= -1.0 # param name is the true name of axis axis = self._geometry.axis_get(self.param_name) axis.value_set(value, self._units) self._geometry.axis_set(self.param_name, axis) @property def fit(self): """True if the parameter can be fit or not""" axis = self._geometry.axis_get(self.param_name) return bool(axis.fit_get()) @fit.setter def fit(self, fit): # param name is the true name of axis axis = self._geometry.axis_get(self.param_name) axis.fit_set(fit) self._geometry.axis_set(self.param_name, axis)