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)