"""Sample on the diffractometer."""
import logging
import numpy as np
from . import util
from .context import TemporaryGeometry
from .engine import Parameter
from .util import Lattice
from .util import libhkl
__all__ = """
check_lattice
HklSample
""".split()
logger = logging.getLogger(__name__)
[docs]
def check_lattice(lattice):
"""Check an Hkl.Lattice for validity
Raises
------
ValueError
"""
# TODO: assertion is raised if alpha/beta/gamma are invalid,
# which is not propagated back as a Python exception but a segfault.
a = lattice.a_get()
b = lattice.b_get()
c = lattice.c_get()
alpha = lattice.alpha_get()
beta = lattice.beta_get()
gamma = lattice.gamma_get()
lt = Lattice(a, b, c, alpha, beta, gamma)
for k, v in lt._asdict().items():
if v is None:
raise ValueError(f'Lattice parameter "{k}" unset or invalid')
lt = Lattice(
a.value_get(util.units["user"]),
b.value_get(util.units["user"]),
c.value_get(util.units["user"]),
alpha.value_get(util.units["user"]),
beta.value_get(util.units["user"]),
gamma.value_get(util.units["user"]),
)
logger.debug("Lattice OK: %s", lt)
[docs]
class HklSample(object):
"""Represents a sample in diffractometer calculations
Parameters
----------
calc : instance of CalcRecip
Reciprocal space calculation class
name : str
A user-defined name used to refer to the sample
sample : Hkl.Sample, optional
A Sample instance from the wrapped Hkl library. Created
automatically if not specified.
units : {'user', 'default'}
Units to use
lattice : np.ndarray, optional
The lattice
U : np.ndarray, optional
The crystal orientation matrix, U
UB : np.ndarray, optional
The UB matrix, where U is the crystal orientation matrix and B is
the transition matrix of a non-orthonormal (the reciprocal of the
crystal) in an orthonormal system
ux : np.ndarray, optional
ux part of the U matrix
uy : np.ndarray, optional
uy part of the U matrix
uz : np.ndarray, optional
uz part of the U matrix
reflections :
All reflections for the current sample in the form::
[(h, k, l), ...]
This assumes the hkl engine is used; generally, the ordered set of
positions for the engine in-use should be specified.
PUBLIC API
.. autosummary::
~name
~add_reflection
~remove_reflection
~swap_orientation_reflections
~clear_reflections
~affine
~compute_UB
~lattice
~reciprocal
~reflection_measured_angles
~reflection_theoretical_angles
~reflections
~reflections_details
~U
~UB
~ux
~uy
~uz
PRIVATE API
.. autosummary::
~hkl_calc
~hkl_sample
~__repr__
~__str__
~_create_reflection
~_get_reflection_dict
~_refl_matrix
~_repr_info
"""
def __init__(self, calc, sample=None, units="user", **kwargs):
if sample is None:
sample = libhkl.Sample.new("")
self._calc = calc
self._sample = sample
self._sample_dict = calc._samples
self._unit_name = units
try:
self._units = util.units[self._unit_name]
except KeyError:
raise ValueError(
f"Unit name '{self._unit_name}' not found. Allowed names: {list(util.units.keys())}"
)
# List of reflections used in computing the U & UB matrices.
# If UB is computed by refinement of more than two reflections,
# the code that sets that up will also need to manage this list.
self._orientation_reflections = []
for name in "lattice name U UB ux uy uz reflections".split():
value = kwargs.pop(name, None)
if value is not None:
try:
setattr(self, name, value)
except Exception as ex:
# These kwargs are funneled down to the gi wrapper
# and could raise just about anything. Tack on the
# kwarg to help debugging if necessary:
ex.message = "%s (attribute=%s)" % (ex, name)
raise
if kwargs:
raise ValueError("Unsupported kwargs for HklSample: %s" % tuple(kwargs.keys()))
@property
def hkl_calc(self):
"""
The HklCalc instance associated with the sample
"""
return self._calc
@property
def hkl_sample(self):
"""
The HKL library sample object
"""
return self._sample
@property
def name(self):
"""
The name of the currently selected sample
"""
return self._sample.name_get()
@name.setter
def name(self, new_name):
"""Replace the current sample
Parameters
----------
new_name : str
"""
if new_name in self._sample_dict:
raise ValueError("Sample with that name already exists")
sample = self._sample
old_name = sample.name_get()
sample.name_set(new_name)
del self._sample_dict[old_name]
self._sample_dict[new_name] = self
@property
def reciprocal(self):
"""The reciprocal lattice"""
lattice = self._sample.lattice_get()
reciprocal = lattice.copy()
lattice.reciprocal(reciprocal)
return reciprocal.get(self._units)
@property
def lattice(self):
"""The lattice (a, b, c, alpha, beta, gamma)
a, b, c [nm]
alpha, beta, gamma [deg]
"""
lattice = self._sample.lattice_get()
lattice = lattice.get(self._units)
return Lattice(*lattice)
@lattice.setter
def lattice(self, lattice):
if not isinstance(lattice, libhkl.Lattice):
a, b, c, alpha, beta, gamma = lattice
alpha, beta, gamma = np.radians((alpha, beta, gamma))
lattice = libhkl.Lattice.new(a, b, c, alpha, beta, gamma)
check_lattice(lattice)
self._sample.lattice_set(lattice)
# TODO: notes mention that lattice should not change, but is it alright
# if init() is called again? or should reflections be cleared,
# etc?
@property
def U(self):
"""
The crystal orientation matrix, U
"""
return util.to_numpy(self._sample.U_get())
@U.setter
def U(self, new_u):
self._orientation_reflections = []
self._sample.U_set(util.to_hkl(new_u))
def _get_parameter(self, param):
return Parameter(param, units=self._unit_name)
@property
def ux(self):
"""
ux part of the U matrix
"""
return self._get_parameter(self._sample.ux_get())
@property
def uy(self):
"""
uy part of the U matrix
"""
return self._get_parameter(self._sample.uy_get())
@property
def uz(self):
"""
uz part of the U matrix
"""
return self._get_parameter(self._sample.uz_get())
@property
def UB(self):
"""
The UB matrix, where U is the crystal orientation matrix and B is the
transition matrix of a non-orthonormal (the reciprocal of the crystal)
in an orthonormal system
If written to, the B matrix will be kept constant:
U * B = UB -> U = UB * B^-1
"""
return util.to_numpy(self._sample.UB_get())
@UB.setter
def UB(self, new_ub):
self._orientation_reflections = []
self._sample.UB_set(util.to_hkl(new_ub))
[docs]
def _create_reflection(self, h, k, l, detector=None):
"""
Create a new reflection with the current geometry/detector
"""
if detector is None:
detector = self._calc._detector
return libhkl.SampleReflection.new(self._calc._geometry, detector, h, k, l)
[docs]
def compute_UB(self, r1, r2):
"""Compute the UB matrix with two reflections.
Using the Busing and Levy method, compute the UB matrix for two sample
reflections, r1 and r2.
Returns the UB matrix or raises gi.repository.GLib.GError
(a change from 0.3.15 and before). Returns ``None`` if no error
raised but computation was not successful.
Parameters
----------
r1 : HklReflection
Reflection 1
r2 : HklReflection
Reflection 2
Returns
-------
UB matrix or raises ``gi.repository.GLib.GError``
"""
if self._sample.compute_UB_busing_levy(r1, r2):
# TODO: this list defines the order of the orientation reflections
self._orientation_reflections = [r1, r2]
return self.UB
@property
def reflections(self):
"""
All reflections for the current sample in the form:
[(h, k, l), ...]
"""
return [refl.hkl_get() for refl in self._sample.reflections_get()]
@reflections.setter
def reflections(self, refls):
self.clear_reflections()
self._orientation_reflections = []
for refl in refls:
self.add_reflection(*refl)
[docs]
def add_reflection(self, h: float, k: float, l: float, position=None, detector=None, compute_ub: bool = False):
"""Add a reflection, optionally specifying the detector to use
Parameters
----------
h : (int, float)
Reflection h
k : (int, float)
Reflection k
l : (int, float)
Reflection l
detector : Hkl.Detector, optional
The detector
position : list, tuple, or namedtuple, optional
The physical motor position that this reflection corresponds to
If not specified, the current geometry of the calculation engine is
assumed.
compute_ub : bool, optional
Calculate the UB matrix with the last two reflections
"""
calc = self._calc
if detector is None:
detector = calc._detector
if compute_ub and len(self.reflections) < 1:
raise RuntimeError("Cannot calculate the UB matrix with less than two reflections")
if compute_ub:
r1 = self._sample.reflections_get()[-1]
with TemporaryGeometry(calc):
def has_valid_position(pos):
"""Raise if invalid, otherwise return boolean."""
if pos is None:
# so use the current motor positions
return False
elif type(pos).__name__.startswith("Pos") or type(pos).__name__.endswith("RealPos"):
# This is (probably) a calc.Position namedtuple
if False in [isinstance(v, (int, float)) for v in pos]:
raise TypeError(f"All values must be numeric, received {pos!r}")
if pos._fields != tuple(calc.physical_axis_names):
# fmt: off
raise KeyError(
f"Wrong axes names. Expected {calc.physical_axis_names},"
f" received {pos._fields}"
)
# fmt: on
return True
elif type(pos).__name__ in "list tuple".split():
# note: isinstance(pos, (list, tuple)) includes namedtuple
if len(pos) != len(calc.physical_axis_names):
# fmt: off
raise ValueError(
f"Expected {len(calc.physical_axis_names)}"
f" positions, received {pos!r}"
)
# fmt: on
if False in [isinstance(v, (int, float)) for v in pos]:
raise TypeError(f"All values must be numeric, received {pos!r}")
return True
elif isinstance(pos, (int, float)):
raise TypeError(f"Expected positions, received {pos!r}")
# fmt: off
raise TypeError(
f"Expected list, tuple, or calc.Position() object,"
f" received {pos!r}"
)
# fmt: on
if has_valid_position(position):
calc.physical_positions = position
r2 = self._sample.add_reflection(calc._geometry, detector, h, k, l)
if compute_ub:
self.compute_UB(r1, r2)
return r2
[docs]
def remove_reflection(self, refl):
"""Remove a specific reflection"""
if not isinstance(refl, libhkl.SampleReflection):
index = self.reflections.index(refl)
refl = self._sample.reflections_get()[index]
return self._sample.del_reflection(refl)
[docs]
def clear_reflections(self):
"""Clear all reflections for the current sample."""
reflections = self._sample.reflections_get()
for refl in reflections:
self._sample.del_reflection(refl)
self._orientation_reflections = []
[docs]
def _refl_matrix(self, fcn):
"""Get a reflection angle matrix."""
sample = self._sample
refl = sample.reflections_get()
refl_matrix = np.zeros((len(refl), len(refl)))
for i, r1 in enumerate(refl):
for j, r2 in enumerate(refl):
if i != j:
refl_matrix[i, j] = fcn(r1, r2)
return refl_matrix
@property
def reflection_measured_angles(self):
return self._refl_matrix(self._sample.get_reflection_measured_angle)
@property
def reflection_theoretical_angles(self):
return self._refl_matrix(self._sample.get_reflection_theoretical_angle)
[docs]
def affine(self):
"""
Refine (affine) the sample lattice parameters from the list of reflections.
"""
return self._sample.affine()
def _repr_info(self):
r = [
f"name={self.name!r}",
f"lattice={self.lattice!r}",
f"ux={self.ux!r}",
f"uy={self.uy!r}",
f"uz={self.uz!r}",
f"U={self.U!r}",
f"UB={self.UB!r}",
f"reflections={self.reflections!r}",
]
return r
def __repr__(self):
return f"{self.__class__.__name__}({', '.join(self._repr_info())})"
def __str__(self):
info = self._repr_info()
info.append(f"reflection_measured_angles={self.reflection_measured_angles!r}")
info.append(f"reflection_theoretical_angles={self.reflection_theoretical_angles!r}")
return f"{self.__class__.__name__}({', '.join(info)}))"
[docs]
def _get_reflection_dict(self, refl):
"""Return dictionary with reflection details."""
h, k, l = refl.hkl_get()
geom = refl.geometry_get()
return {
"reflection": {"h": h, "k": k, "l": l},
"flag": refl.flag_get(), # not used by hklpy
"wavelength": geom.wavelength_get(1),
"position": dict(zip(geom.axis_names_get(), geom.axis_values_get(1))),
"orientation_reflection": refl in self._orientation_reflections,
}
@property
def reflections_details(self):
"""
Return a list with details of all reflections.
NOTE: reflections_details() uses the canonical
names for the real positioners. The mapping
to physical axis names happens in :mod`hkl.calc`.
"""
refls = self._sample.reflections_get()
for r in self._orientation_reflections:
if r not in refls:
# Edge case when orientation reflection was
# deleted from the list in libhkl.
refls.append(r)
return [self._get_reflection_dict(r) for r in refls]
[docs]
def swap_orientation_reflections(self):
"""Swap the 2 [UB] reflections, re-compute & return new [UB]."""
if len(self._orientation_reflections) != 2:
raise ValueError(
"Must have exactly 2 orientation reflections defined"
" in order to make a swap and re-compute [UB]."
f" There are {len(self._orientation_reflections)}"
" orientation reflection(s) defined now."
)
refls = self._orientation_reflections
return self.compute_UB(*refls[::-1])