"""
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)