Files
furka/qspace.py
2023-05-10 15:05:22 +02:00

378 lines
11 KiB
Python

from types import SimpleNamespace
from diffcalc.hkl.calc import HklCalculation
from diffcalc.hkl.constraints import Constraints
from diffcalc.hkl.geometry import Position
from diffcalc.ub.calc import UBCalculation
from slic.core.adjustable import Adjustable, PVAdjustable
from slic.core.device import Device, SimpleDevice
from slic.devices.general.motor import Motor
from slic.utils.printing import printable_table
from constraints import ExtraConstraint
INDICES = {
"h": 0,
"k": 1,
"l": 2
}
class QSpace3D(Device):
def __init__(self, ID, mu, chi, phi, nu, wavelength, **kwargs): # if the diffcalc objects need some initial parameters, add them here and fill them in below...
super().__init__(ID, **kwargs)
# collect the motors in a device for nicer printing
self.motors = motors = SimpleDevice(
ID,
mu = mu,
chi = chi,
phi = phi,
nu = nu
)
self.wavelength = wavelength
# some diffcalc objects ...
ub = UBCalculation()
cons = Constraints()
hkl = HklCalculation(ub, cons)
# ... collected in a namespace
self.dc = dc = SimpleNamespace(
ub = ub,
cons = cons,
hkl = hkl
)
# and the individual coordinates:
self.h = QSpace1D(ID + "-H", "h", dc, motors, wavelength)
self.k = QSpace1D(ID + "-K", "k", dc, motors, wavelength)
self.l = QSpace1D(ID + "-L", "l", dc, motors, wavelength)
# it might be nice (but optional) to add some shortcuts like this:
def set_lattice(self, *args, **kwargs):
self.dc.ub.set_lattice(*args, **kwargs)
def add_orientation(self, *args, **kwargs):
self.dc.ub.add_orientation(*args, **kwargs)
def del_orientation(self, *args):
self.dc.ub.del_orientation(*args)
def add_reflection(self, *args, **kwargs):
self.dc.ub.add_reflection(*args, **kwargs)
def del_reflection(self, *args):
self.dc.ub.del_reflection(*args)
def calc_ub(self, *args):
self.dc.ub.calc_ub(*args) # not sure whether this needs to be called explicitly?
def fit_ub(self, *args, **kwargs):
self.dc.ub.fit_ub(*args, **kwargs)
@property
def UB(self):
self.dc.ub.UB
def clear_constraints(self):
self.dc.cons.clear()
def set_constraints(self, **kwargs):
self.clear_constraints()
for name, value in kwargs.items():
setattr(self.dc.cons, name, value)
def get_position(self, *args, extra_cons):
wl = self.wavelength.get_current_value()
res = self.dc.hkl.get_position(*args, wl)
if extra_cons:
res = [r for r in res if extra_cons(_flatten_get_position_result(r))]
return res
def hard_constraints(x):
if x["betain"]<0:
return False
if x["betaout"]<0:
return False
if x["nu"]<0:
return False
if x["nu"]>180:
return False
if x["mu"]<0:
return False
if x["mu"]>90:
return False
if x["chi"]<-30:
return False
if x["chi"]>30:
return False
if x["phi"]<-60:
return False
if x["phi"]>60:
return False
return True
# if I understand the examples/code correctly, then some more method calls are mandatory?
# those should probably all get shortcuts...
def _get_hkl(self):
angles = self._get_angles()
return self._calc_hkl(*angles)
def _get_angles(self):
ms = self.motors
mu = ms.mu.get_current_value()
chi = ms.chi.get_current_value()
phi = ms.phi.get_current_value()
nu = ms.nu.get_current_value()
return mu, chi, phi, nu
def _calc_hkl(self, mu, chi, phi, nu):
wl = self.get_wavelength()
pos = Position(mu=mu, chi=chi, phi=phi, nu=nu)
hkl = self.dc.hkl.get_hkl(pos, wl)
return hkl
def get_wavelength(self):
return self.wavelength.get_current_value()
def _set_hkl(self, h, k, l, extra_cons):
angles = self._calc_angles(h, k, l, extra_cons)
self._set_angles(*angles)
def _set_angles(self, mu, chi, phi, nu):
ms = self.motors
t_mu = ms.mu.set_target_value(90+mu)
t_chi = ms.chi.set_target_value(chi-180)
t_phi = ms.phi.set_target_value(phi)
t_nu = ms.nu.set_target_value(180+nu) # We may need a correct offset value
# wait for all motors to arrive
tasks = (t_mu, t_chi, t_phi, t_nu)
for t in tasks:
t.wait()
# def _set_angles(self, mu, chi, phi, nu):
# ms = self.motors
# t_mu = ms.mu.set_target_value(90-mu)
# t_chi = ms.chi.set_target_value(chi)
# t_phi = ms.phi.set_target_value(phi)
# t_nu = ms.nu.set_target_value(nu)
# wait for all motors to arrive
# tasks = (t_mu, t_chi, t_phi, t_nu)
# for t in tasks:
# t.wait()
# def _calc_angles(self, h, k, l):
# wl = self.get_wavelength()
# pos, _virtual_angles = next(iter(
# self.dc.hkl.get_position(h, k, l, wl)
# ))
# return pos.mu, pos.chi, pos.phi, pos.nu
def _calc_angles(self, h, k, l, extra_cons):
wl = self.get_wavelength()
pos, _virtual_angles = next(iter(
self.get_position(h, k, l, extra_cons=extra_cons)
))
return pos.mu, pos.chi, pos.phi, pos.nu
def _flatten_get_position_result(x):
d0, d1 = x
d0 = d0.asdict
return dict(**d0, **d1)
class QSpace1D(Adjustable):
def __init__(self, ID, index, dc, motors, wavelength):
super().__init__(ID)
self.index = index
self.dc = dc
self.motors = motors
self.wavelength = wavelength
# the following three methods are mandatory:
def get_current_value(self):
hkl = self._get_hkl()
i = self._get_index()
return hkl[i]
def set_target_value(self, value, extra_cons):
# get all current indices
hkl = self._get_hkl()
hkl = list(hkl)
i = self._get_index()
# insert the target value into the right spot
hkl[i] = value
self._set_hkl(*hkl,extra_cons)
def is_moving(self):
ms = self.motors
return ms.mu.moving or ms.chi.moving or ms.phi.moving
# some helpful things:
def get_position(self, *args, extra_cons):
wl = self.wavelength.get_current_value()
res = self.dc.hkl.get_position(*args, wl)
if extra_cons:
res = [r for r in res if extra_cons(_flatten_get_position_result(r))]
return res
def get_wavelength(self):
return self.wavelength.get_current_value()
def _get_index(self):
i = self.index
if isinstance(i, str):
i = i.casefold()
i = INDICES.get(i, i)
if i not in INDICES.values():
allowed = INDICES.keys() | INDICES.values()
allowed = sorted(allowed, key=str)
raise ValueError(f"index must be from {allowed} but is {repr(i)}")
return i
def _get_hkl(self):
angles = self._get_angles()
return self._calc_hkl(*angles)
def _set_hkl(self, h, k, l, extra_cons):
angles = self._calc_angles(h, k, l, extra_cons)
self._set_angles(*angles)
def _get_angles(self):
ms = self.motors
mu = ms.mu.get_current_value()
chi = ms.chi.get_current_value()
phi = ms.phi.get_current_value()
nu = ms.nu.get_current_value()
return mu, chi, phi, nu
def _set_angles(self, mu, chi, phi, nu):
ms = self.motors
t_mu = ms.mu.set_target_value(mu)
t_chi = ms.chi.set_target_value(chi)
t_phi = ms.phi.set_target_value(phi)
t_nu = ms.nu.set_target_value(nu)
# wait for all motors to arrive
tasks = (t_mu, t_chi, t_phi, t_nu)
for t in tasks:
t.wait()
def _calc_hkl(self, mu, chi, phi, nu):
wl = self.get_wavelength()
pos = Position(mu=mu, chi=chi, phi=phi, nu=nu)
hkl = self.dc.hkl.get_hkl(pos, wl)
return hkl
# def _calc_angles(self, h, k, l):
# wl = self.get_wavelength()
# pos, _virtual_angles = next(iter(
# self.dc.hkl.get_position(h, k, l, wl)
# ))
# return pos.mu, pos.chi, pos.phi, pos.nu
def _calc_angles(self, h, k, l, extra_cons):
wl = self.get_wavelength()
pos, _virtual_angles = next(iter(
self.get_position(h, k, l, extra_cons)
))
return pos.mu, pos.chi, pos.phi, pos.nu
h = 6.62607015e-34 # J s
c = 299792458 # m / s
e = 1.60217663e-19 # C
f = h * c / e * 1e-3 * 1e9 *1e4# E [eV] = f / lambda [nm]
class Wavelength(Adjustable):
def __init__(self, energy):
self.energy = energy
# assert self.energy.units == "keV" # otherwise conversion is wrong
super().__init__(energy.ID + "_WL", name=energy.name + " as Wavelength", units="nm")
def get_current_value(self):
E = self.energy.get_current_value()
return f / E
def set_target_value(self, value):
E = f / value
return self.energy.set_target_value(E)
def is_moving(self):
return self.energy.is_moving()
class HistoryDummy(Adjustable):
def __init__(self, ID, initial_value=None, **kwargs):
super().__init__(ID, **kwargs)
self.history = [] if initial_value is None else [initial_value]
@classmethod
def init_from(cls, adj):
ID = "HD:" + adj.ID
initial_value = adj.get_current_value()
name = cls.__name__ + " for " + adj.name
units = adj.units
return cls(ID, initial_value=initial_value, name=name, units=units)
def get_current_value(self):
try:
return self.history[-1]
except IndexError:
return None
def set_target_value(self, value):
self.history.append(value)
def is_moving(self):
return False
def print_history(*adjs, enumerate_lines=True, make_legend=True):
labels = []
histories = []
for a in adjs:
try:
h = a.history
except AttributeError:
continue
labels.append(repr(a))
histories.append(h)
print(printable_table(histories, labels, enumerate_lines=enumerate_lines, make_legend=make_legend))
## these point to the different motors
#mu = Motor("SATES30-RIXS:MOT_SRY.VAL")
#chi = Motor("SATES30-RIXS:MOT_SRZ.VAL")
#phi = Motor("SATES30-RIXS:MOT_SRX.VAL")
#nu = Motor("SATES30-RIXS:MOT_DRY.VAL")
## and this to the machine wavelength (maybe this needs to be calculated from the energy? then we should add a Wavelength wrapper...)
#wl = Wavelength(PVAdjustable("MACHINE:ENERGY"))
## put it all together:
#q = QSpace3D("SOMETHING:Q", mu, chi, phi, nu, wl)
## in ipython
#q.set_lattice("SiO2", 4.913, 5.405)
#q.set_target_value(1.5)