Demo: hkl_soleil Python API#

Exercise Hkl’s (libhkl) Python API.

Note: This demo is a work-in-progress. It does not use the hklpy2 package (other than to load libhkl).

[1]:
from libhkl_python_api import Diffractometer, Table
from pprint import pprint
import numpy

Create diffractometer object#

[2]:
e4cv = Diffractometer("E4CV", engine="hkl")

a0 = 5.431
e4cv.wavelength = 1.54
e4cv.sample_name = "silicon"
e4cv.lattice = a0, a0, a0, 90, 90, 90
e4cv.mode = "psi_constant"

e4cv.angles = (30, 0, 0, 60)
e4cv.extras = dict(h2=1, k2=0, l2=0, psi=0)
e4cv.pseudos = (1, 0, 1)

pprint(e4cv.info)
{'Hkl': 'v5.0.0.3434',
 'engine': 'hkl',
 'geometry': 'E4CV',
 'lattice': {'a': 5.431,
             'alpha': 90.0,
             'b': 5.431,
             'beta': 90.0,
             'c': 5.431,
             'gamma': 90.0},
 'mode': 'psi_constant',
 'sample': 'silicon',
 'wavelength': 1.54}
[3]:
e4cv.wh
h        k        l
1.0      0.0      1.0

omega    chi      phi      tth
30.0     0.0      0.0      60.0

Scan angle \(\psi\) around virtual \((h_2 k_2 l_2)\) axis in psi_constant mode#

[4]:
start, finish, np = -140.11, 140.0, 16
e4cv.mode = "psi_constant"
print(f"Scan psi from {start} to {finish} with {np} points. {e4cv.mode=!r}")
table = Table()
for psi in numpy.linspace(start, finish, num=np):
    e4cv.extras = dict(psi=round(psi, ndigits=1))  # only update psi
    e4cv.forward(1, 0, 1)
    if len(e4cv.solutions) > 0:
        results = e4cv.pseudos
        results.update(e4cv.extras)
        results.update(e4cv.solutions[0])
        table.add(results)
print(table)
Scan psi from -140.11 to 140.0 with 16 points. e4cv.mode='psi_constant'
=== === === === === === ====== ======= ====== ===== ======
h   k   l   h2  k2  l2  psi    omega   chi    phi   tth
=== === === === === === ====== ======= ====== ===== ======
1.0 0.0 1.0 1.0 0.0 0.0 -140.1 101.567 -140.1 -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -121.4 101.567 -121.4 -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -102.8 101.567 -102.8 -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -84.1  101.567 -84.1  -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -65.4  101.567 -65.4  -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -46.7  101.567 -46.7  -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -28.1  101.567 -28.1  -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 -9.4   101.567 -9.4   -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 9.3    101.567 9.3    -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 28.0   101.567 28.0   -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 46.6   101.567 46.6   -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 65.3   101.567 65.3   -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 84.0   101.567 84.0   -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 102.7  101.567 102.7  -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 121.3  101.567 121.3  -45.0 23.133
1.0 0.0 1.0 1.0 0.0 0.0 140.0  101.567 140.0  -45.0 23.133
=== === === === === === ====== ======= ====== ===== ======

Scan \(\omega\) axis in constant_omega mode#

[5]:
omega = 27.5  # a somewhat arbitrary choice for this demo
chi, phi, tth = 0, 0, 60
e4cv.mode = "constant_omega"
e4cv.angles = omega, chi, phi, tth
[6]:
pprint(e4cv.info)
{'Hkl': 'v5.0.0.3434',
 'engine': 'hkl',
 'geometry': 'E4CV',
 'lattice': {'a': 5.431,
             'alpha': 90.0,
             'b': 5.431,
             'beta': 90.0,
             'c': 5.431,
             'gamma': 90.0},
 'mode': 'constant_omega',
 'sample': 'silicon',
 'wavelength': 1.54}
[7]:
start, finish, np = -1.03, 1.0001, 16
e4cv.mode = "constant_omega"
print(f"Scan h from {start} to {finish} with {np} points. {e4cv.mode=!r}")
table = Table()
for h1 in numpy.linspace(start, finish, num=np):
    e4cv.forward(h1, 0, 1)
    reals = e4cv.solutions[0]
    results = e4cv.pseudos
    results.update(reals)
    table.add(results)
print(table)
Scan h from -1.03 to 1.0001 with 16 points. e4cv.mode='constant_omega'
======= === === ===== === ======= ======
h       k   l   omega chi phi     tth
======= === === ===== === ======= ======
-1.03   0.0 1.0 27.5  0.0 -61.603 23.488
-0.8947 0.0 1.0 27.5  0.0 -58.351 21.933
-0.7593 0.0 1.0 27.5  0.0 -54.456 20.509
-0.624  0.0 1.0 27.5  0.0 -49.843 19.24
-0.4886 0.0 1.0 27.5  0.0 -44.463 18.158
-0.3533 0.0 1.0 27.5  0.0 -38.31  17.296
-0.218  0.0 1.0 27.5  0.0 -31.452 16.687
-0.0826 0.0 1.0 27.5  0.0 -24.044 16.357
0.0527  0.0 1.0 27.5  0.0 -16.32  16.324
0.1881  0.0 1.0 27.5  0.0 -8.555  16.589
0.3234  0.0 1.0 27.5  0.0 -1.009  17.139
0.4587  0.0 1.0 27.5  0.0 6.117   17.948
0.5941  0.0 1.0 27.5  0.0 12.706  18.984
0.7294  0.0 1.0 27.5  0.0 18.715  20.214
0.8648  0.0 1.0 27.5  0.0 24.155  21.607
1.0001  0.0 1.0 27.5  0.0 29.07   23.134
======= === === ===== === ======= ======

Diffractometer adapter library#

[8]:
%pycat libhkl_python_api.py
"""
Exercise Hkl's (libhkl) Python API.
"""

import numpy
import pyRestTable
from gi.repository import GLib  # noqa: F401
import gi

gi.require_version("Hkl", "5.0")
from gi.repository import Hkl  # noqa: E402

DEFAULT_DIGITS = 9
UNITS = Hkl.UnitEnum.USER


class Table(pyRestTable.Table):
    def add(self, dd: dict):
        keys = list(dd.keys())
        if len(self.labels) == 0:
            self.labels = keys
        if keys == self.labels:
            self.addRow(list(dd.values()))
        else:
            KeyError(
                "All rows must have same keys."
                f"  Received {keys!r}, expected {self.labels!r}."
            )


def to_hkl(arr):
    import numpy as np

    if isinstance(arr, Hkl.Matrix):
        return arr

    arr = np.array(arr)

    hklm = Hkl.Matrix.new_euler(0, 0, 0)
    hklm.init(*arr.flatten())
    return hklm


def to_numpy(mat) -> numpy.ndarray:
    if isinstance(mat, numpy.ndarray):
        return mat

    ret = numpy.zeros((3, 3))
    for i in range(3):
        for j in range(3):
            ret[i, j] = mat.get(i, j)

    return ret


class Diffractometer:
    def __init__(self, geometry, engine="hkl") -> None:
        self.detector = Hkl.Detector.factory_new(Hkl.DetectorType(0))
        self.factory = Hkl.factories()[geometry]
        self.engines = self.factory.create_new_engine_list()
        self.geometry = self.factory.create_new_geometry()
        self.sample = Hkl.Sample.new("sample")
        self.engines.init(self.geometry, self.detector, self.sample)
        self.engine = self.engines.engine_get_by_name(engine)
        self._solutions = []

    def _get_as_dict(
        self, obj: object, part: str, digits: int = DEFAULT_DIGITS
    ) -> dict:
        names = getattr(obj, f"{part}_names_get")()
        values = getattr(obj, f"{part}_values_get")(UNITS)
        return dict(zip(names, self._roundoff(values, digits=digits)))

    def _roundoff(self, array: list, digits: int = 9):
        return [round(value, ndigits=digits) or 0.0 for value in array]

    def forward(self, *pseudos: list) -> list:
        self._solutions = self.engine.pseudo_axis_values_set(
            pseudos,
            UNITS,
        )
        return self._solutions

    def inverse(self, *reals: list) -> dict:
        self.angles = reals
        self.engines.get()  # reals -> pseudos  (Odd name for this call!)
        return self.pseudos

    @property
    def angles(self) -> dict:
        return self._get_as_dict(self.geometry, "axis")

    @angles.setter
    def angles(self, values: list) -> None:
        self.geometry.axis_values_set(values, UNITS)

    @property
    def extras(self) -> dict:
        return self._get_as_dict(self.engine, "parameters")

    @extras.setter
    def extras(self, pdict: dict) -> None:
        pnames = self.engine.parameters_names_get()
        for k, v in pdict.items():
            if k not in pnames:
                raise KeyError(f"Unknown parameter name {k!r}.")

            p = self.engine.parameter_get(k)
            p.value_set(v, UNITS)
            self.engine.parameter_set(k, p)

    @property
    def info(self) -> dict:
        result = dict(
            Hkl=Hkl.VERSION,
            # numpy=numpy.__version__,
            geometry=self.geometry.name_get(),
            engine=self.engine.name_get(),
            mode=self.mode,
            wavelength=self.wavelength,
            sample=self.sample_name,
            lattice=self.lattice,
        )
        return result

    @property
    def lattice(self) -> dict:
        lattice = self.sample.lattice_get()
        lattice = lattice.get(UNITS)
        lattice = {k: getattr(lattice, k) for k in "a b c alpha beta gamma".split()}
        return lattice

    @lattice.setter
    def lattice(self, parameters: dict) -> None:
        a, b, c, alpha, beta, gamma = parameters

        lattice = self.sample.lattice_get()
        lattice.set(a, b, c, alpha, beta, gamma, UNITS)
        self.sample.lattice_set(lattice)

    @property
    def mode(self) -> str:
        return self.engine.current_mode_get()

    @mode.setter
    def mode(self, value: str) -> None:
        self.engine.current_mode_set(value)

    @property
    def modes(self) -> list:
        return self.engine.modes_names_get()

    @property
    def pseudos(self) -> dict:
        return self._get_as_dict(self.engine, "pseudo_axis", digits=4)

    @pseudos.setter
    def pseudos(self, values: list) -> None:
        self.engine.pseudo_axis_values_set(
            values,
            UNITS,
        )

    @property
    def sample_name(self) -> str:
        return self.sample.name_get()

    @sample_name.setter
    def sample_name(self, value: str) -> None:
        self.sample.name_set(value)

    @property
    def solutions(self) -> list:
        def sdict(sol):
            geo = sol.geometry_get()
            return self._get_as_dict(geo, "axis", digits=3)

        return [sdict(sol) for sol in self._solutions.items()]

    @property
    def UB(self) -> list:
        matrix = to_numpy(self.sample.UB_get())
        return matrix.round(decimals=5).tolist()

    @UB.setter
    def UB(self, values: list[list[float]]) -> None:
        self.sample.UB_set(to_hkl(values))

    @property
    def wavelength(self) -> float:
        return self.geometry.wavelength_get(UNITS)

    @wavelength.setter
    def wavelength(self, value: float) -> None:
        self.geometry.wavelength_set(value, UNITS)

    @property
    def wh(self) -> None:
        def show(axes):
            keys, values = [], []
            for k, v in axes.items():
                keys.append(f"{k:8s}")
                values.append(f"{str(round(v, ndigits=3) or 0.0):8s}")
            print(" ".join(keys))
            print(" ".join(values))
            print()

        show(self.pseudos)
        show(self.angles)


def psi_scan(start, finish, np):
    e4cv = Diffractometer("E4CV", engine="hkl")

    a0 = 5.431
    e4cv.wavelength = 1.54
    e4cv.sample_name = "silicon"
    e4cv.lattice = a0, a0, a0, 90, 90, 90
    e4cv.mode = "psi_constant"

    e4cv.angles = (30, 0, 0, 60)
    e4cv.extras = dict(h2=1, k2=0, l2=0, psi=0)
    e4cv.pseudos = (1, 0, 1)

    print(e4cv.info)
    print(f"{e4cv.UB=!r}")
    e4cv.wh

    print()
    print(f"Scan psi from {start} to {finish} with {np} points. {e4cv.mode=!r}")
    table = Table()
    for psi in numpy.linspace(start, finish, num=np):
        e4cv.extras = dict(psi=round(psi, ndigits=1))  # only update psi
        e4cv.forward(1, 0, 1)
        if len(e4cv.solutions) > 0:
            results = e4cv.pseudos
            results.update(e4cv.extras)
            results.update(e4cv.solutions[0])
            table.add(results)
    print(table)


def omega_constant(omega):
    e4cv = Diffractometer("E4CV", engine="hkl")

    a0 = 5.431
    e4cv.wavelength = 1.54
    e4cv.sample_name = "silicon"
    e4cv.lattice = a0, a0, a0, 90, 90, 90
    e4cv.mode = "constant_omega"

    e4cv.angles = (omega, 0, 0, 60)

    e4cv.forward(1, 0, 1)
    if len(e4cv.solutions) > 0:
        results = e4cv.pseudos
        results.update(e4cv.solutions[0])
        print(results)

    print(e4cv.info)

    reals = e4cv.solutions[0]
    print(f"{reals=!r}")
    print(f"{e4cv.inverse(*list(reals.values()))=!r}")

    start, finish, np = -1.00028, 1.0001, 16
    print()
    print(f"Scan h from {start} to {finish} with {np} points. {e4cv.mode=!r}")
    table = Table()
    for h1 in numpy.linspace(start, finish, num=np):
        e4cv.forward(h1, 0, 1)
        reals = e4cv.solutions[0]
        results = e4cv.pseudos
        results.update(reals)
        table.add(results)
    print(table)


if __name__ == "__main__":
    psi_scan(-140.0018, 140.01, 16)
    omega_constant(35)