fix: lamni scan adjustments and refactor #198
@@ -1,6 +1,11 @@
|
||||
from .alignment import XrayEyeAlign
|
||||
from .lamni_alignment_mixin import LamNIAlignmentMixin
|
||||
from .lamni import LamNI
|
||||
from .lamni_optics_mixin import LamNIInitError, LaMNIInitStages, LamNIOpticsMixin
|
||||
|
||||
__all__ = [
|
||||
"LamNI", "XrayEyeAlign", "LamNIInitError", "LaMNIInitStages", "LamNIOpticsMixin"
|
||||
]
|
||||
"LamNI",
|
||||
"LamNIAlignmentMixin",
|
||||
"LamNIInitError",
|
||||
"LaMNIInitStages",
|
||||
"LamNIOpticsMixin",
|
||||
]
|
||||
|
||||
@@ -1,461 +0,0 @@
|
||||
import builtins
|
||||
import time
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from typeguard import typechecked
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
umv = builtins.__dict__.get("umv")
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
# pixel calibration, multiply to get mm
|
||||
# PIXEL_CALIBRATION = 0.2/209 #.2 with binning
|
||||
PIXEL_CALIBRATION = 0.2 / 218 # .2 with binning
|
||||
|
||||
def __init__(self, client, lamni) -> None:
|
||||
self.client = client
|
||||
self.lamni = lamni
|
||||
self.device_manager = client.device_manager
|
||||
self.scans = client.scans
|
||||
self.alignment_values = defaultdict(list)
|
||||
self._reset_init_values()
|
||||
self.corr_pos_x = []
|
||||
self.corr_pos_y = []
|
||||
self.corr_angle = []
|
||||
self.corr_pos_x_2 = []
|
||||
self.corr_pos_y_2 = []
|
||||
self.corr_angle_2 = []
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Correction reset
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def reset_correction(self):
|
||||
self.corr_pos_x = []
|
||||
self.corr_pos_y = []
|
||||
self.corr_angle = []
|
||||
|
||||
def reset_correction_2(self):
|
||||
self.corr_pos_x_2 = []
|
||||
self.corr_pos_y_2 = []
|
||||
self.corr_angle_2 = []
|
||||
|
||||
def reset_xray_eye_correction(self):
|
||||
self.client.delete_global_var("tomo_fit_xray_eye")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# FOV offset properties
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def tomo_fovx_offset(self):
|
||||
val = self.client.get_global_var("tomo_fov_offset")
|
||||
if val is None:
|
||||
return 0.0
|
||||
return val[0] / 1000
|
||||
|
||||
@tomo_fovx_offset.setter
|
||||
@typechecked
|
||||
def tomo_fovx_offset(self, val: float):
|
||||
val_old = self.client.get_global_var("tomo_fov_offset")
|
||||
if val_old is None:
|
||||
val_old = [0.0, 0.0]
|
||||
self.client.set_global_var("tomo_fov_offset", [val * 1000, val_old[1]])
|
||||
|
||||
@property
|
||||
def tomo_fovy_offset(self):
|
||||
val = self.client.get_global_var("tomo_fov_offset")
|
||||
if val is None:
|
||||
return 0.0
|
||||
return val[1] / 1000
|
||||
|
||||
@tomo_fovy_offset.setter
|
||||
@typechecked
|
||||
def tomo_fovy_offset(self, val: float):
|
||||
val_old = self.client.get_global_var("tomo_fov_offset")
|
||||
if val_old is None:
|
||||
val_old = [0.0, 0.0]
|
||||
self.client.set_global_var("tomo_fov_offset", [val_old[0], val * 1000])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0, 0]
|
||||
self._xray_fov_xy = [0, 0]
|
||||
|
||||
def _disable_rt_feedback(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_disable()
|
||||
|
||||
def _enable_rt_feedback(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_enable_with_reset()
|
||||
|
||||
def tomo_rotate(self, val: float):
|
||||
# pylint: disable=undefined-variable
|
||||
umv(self.device_manager.devices.lsamrot, val)
|
||||
|
||||
def get_tomo_angle(self):
|
||||
return self.device_manager.devices.lsamrot.readback.read()["lsamrot"]["value"]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X-ray eye camera control
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def save_frame(self):
|
||||
epics_put("XOMNYI-XEYE-SAVFRAME:0", 1)
|
||||
|
||||
def update_frame(self):
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
# start live
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 1)
|
||||
# wait for start live
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
time.sleep(0.5)
|
||||
print("waiting for live view to start...")
|
||||
fshopen()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
print("waiting for new frame...")
|
||||
time.sleep(0.5)
|
||||
|
||||
time.sleep(0.5)
|
||||
# stop live view
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
||||
time.sleep(1)
|
||||
print("got new frame")
|
||||
|
||||
def update_fov(self, k: int):
|
||||
self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0])
|
||||
self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0])
|
||||
|
||||
@property
|
||||
def movement_buttons_enabled(self):
|
||||
return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")]
|
||||
|
||||
@movement_buttons_enabled.setter
|
||||
def movement_buttons_enabled(self, enabled: bool):
|
||||
enabled = int(enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVY:0", enabled)
|
||||
|
||||
def send_message(self, msg: str):
|
||||
epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Alignment procedure
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def align(self):
|
||||
self._reset_init_values()
|
||||
self.reset_correction()
|
||||
self.reset_correction_2()
|
||||
|
||||
self._disable_rt_feedback()
|
||||
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
|
||||
self._enable_rt_feedback()
|
||||
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
||||
self.send_message("please wait...")
|
||||
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", "Let us LAMNI...")
|
||||
|
||||
self._disable_rt_feedback()
|
||||
k = 0
|
||||
|
||||
self.lamni.lfzp_in()
|
||||
self.update_frame()
|
||||
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-STEP:0", 0)
|
||||
self.send_message("Submit center value of FZP.")
|
||||
|
||||
while True:
|
||||
if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1:
|
||||
val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") * self.PIXEL_CALIBRATION # in mm
|
||||
val_y = epics_get(f"XOMNYI-XEYE-YVAL_Y:{k}") * self.PIXEL_CALIBRATION # in mm
|
||||
self.alignment_values[k] = [val_x, val_y]
|
||||
print(
|
||||
f"Clicked position {k}: x {self.alignment_values[k][0]}, y"
|
||||
f" {self.alignment_values[k][1]}"
|
||||
)
|
||||
|
||||
if k == 0: # received center value of FZP
|
||||
self.send_message("please wait ...")
|
||||
self.lamni.loptics_out()
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
self.movement_buttons_enabled = False
|
||||
print("Moving sample in, FZP out")
|
||||
|
||||
self._disable_rt_feedback()
|
||||
time.sleep(0.3)
|
||||
self._enable_rt_feedback()
|
||||
time.sleep(0.3)
|
||||
|
||||
self.update_frame()
|
||||
self.send_message("Go and find the sample")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
self.movement_buttons_enabled = True
|
||||
|
||||
elif k == 1: # received sample center value at samrot 0
|
||||
msg = (
|
||||
f"Base shift values from movement are x {self.shift_xy[0]}, y"
|
||||
f" {self.shift_xy[1]}"
|
||||
)
|
||||
print(msg)
|
||||
logger.info(msg)
|
||||
self.shift_xy[0] += (
|
||||
self.alignment_values[0][0] - self.alignment_values[1][0]
|
||||
) * 1000
|
||||
self.shift_xy[1] += (
|
||||
self.alignment_values[1][1] - self.alignment_values[0][1]
|
||||
) * 1000
|
||||
print(
|
||||
"Base shift values from movement and clicked position are x"
|
||||
f" {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
)
|
||||
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
).wait()
|
||||
|
||||
self.send_message("please wait ...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
self.movement_buttons_enabled = False
|
||||
time.sleep(1)
|
||||
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
).wait()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
self.update_frame()
|
||||
self.send_message("Submit sample center and FOV (0 deg)")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
self.update_fov(k)
|
||||
|
||||
elif 1 < k < 10: # received sample center value at samrot 0 ... 315
|
||||
self.send_message("please wait ...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate((k - 1) * 45 - 45 / 2)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
).wait()
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate((k - 1) * 45)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
).wait()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
self.update_frame()
|
||||
self.send_message("Submit sample center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", 1)
|
||||
self.update_fov(k)
|
||||
|
||||
elif k == 10: # received sample center value at samrot 270, done
|
||||
self.send_message("done...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
self.movement_buttons_enabled = False
|
||||
self.update_fov(k)
|
||||
break
|
||||
|
||||
k += 1
|
||||
epics_put("XOMNYI-XEYE-STEP:0", k)
|
||||
|
||||
if k < 2:
|
||||
_xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0")
|
||||
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
|
||||
if _xrayeyalignmvx != 0 or _xrayeyalignmvy != 0:
|
||||
self.shift_xy[0] = self.shift_xy[0] + _xrayeyalignmvx
|
||||
self.shift_xy[1] = self.shift_xy[1] + _xrayeyalignmvy
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle()
|
||||
).wait()
|
||||
print(
|
||||
f"Current center horizontal {self.shift_xy[0]} vertical {self.shift_xy[1]}"
|
||||
)
|
||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
||||
epics_put("XOMNYI-XEYE-MVX:0", 0)
|
||||
self.update_frame()
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
self.write_output()
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
print(
|
||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns,"
|
||||
f" fovy = {fovy:.0f} microns"
|
||||
)
|
||||
print("Use matlab routine to fit the current alignment...")
|
||||
print(
|
||||
"This additional shift is applied to the base shift values\n which are x"
|
||||
f" {self.shift_xy[0]}, y {self.shift_xy[1]}"
|
||||
)
|
||||
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(0)
|
||||
|
||||
print(
|
||||
"\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running"
|
||||
" lamni.align.read_xray_eye_correction()\n"
|
||||
)
|
||||
|
||||
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Alignment output
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def write_output(self):
|
||||
import os
|
||||
with open(
|
||||
os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues"), "w"
|
||||
) as alignment_values_file:
|
||||
alignment_values_file.write("angle\thorizontal\tvertical\n")
|
||||
for k in range(2, 11):
|
||||
fovx_offset = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000
|
||||
fovy_offset = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000
|
||||
print(
|
||||
f"Writing to file new alignment: number {k}, value x {fovx_offset}, y"
|
||||
f" {fovy_offset}"
|
||||
)
|
||||
alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X-ray eye sinusoidal correction (loaded from MATLAB fit files)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def read_xray_eye_correction(self, dir_path=None):
|
||||
import os
|
||||
if dir_path is None:
|
||||
dir_path = os.path.expanduser("~/Data10/specES1/internal/")
|
||||
tomo_fit_xray_eye = np.zeros((2, 3))
|
||||
for i, axis in enumerate(["x", "y"]):
|
||||
for j, coeff in enumerate(["A", "B", "C"]):
|
||||
with open(os.path.join(dir_path, f"ptychotomoalign_{coeff}{axis}.txt"), "r") as f:
|
||||
tomo_fit_xray_eye[i][j] = f.readline()
|
||||
|
||||
self.client.set_global_var("tomo_fit_xray_eye", tomo_fit_xray_eye.tolist())
|
||||
# x amp, phase, offset, y amp, phase, offset
|
||||
# 0 0 0 1 0 2 1 0 1 1 1 2
|
||||
print("New alignment parameters loaded from X-ray eye")
|
||||
print(
|
||||
f"X Amplitude {tomo_fit_xray_eye[0][0]}, "
|
||||
f"X Phase {tomo_fit_xray_eye[0][1]}, "
|
||||
f"X Offset {tomo_fit_xray_eye[0][2]}, "
|
||||
f"Y Amplitude {tomo_fit_xray_eye[1][0]}, "
|
||||
f"Y Phase {tomo_fit_xray_eye[1][1]}, "
|
||||
f"Y Offset {tomo_fit_xray_eye[1][2]}"
|
||||
)
|
||||
|
||||
def lamni_compute_additional_correction_xeye_mu(self, angle):
|
||||
"""Compute sinusoidal correction from the X-ray eye fit for the given angle."""
|
||||
tomo_fit_xray_eye = self.client.get_global_var("tomo_fit_xray_eye")
|
||||
if tomo_fit_xray_eye is None:
|
||||
print("Not applying any additional correction. No x-ray eye data available.\n")
|
||||
return (0, 0)
|
||||
|
||||
# x amp, phase, offset, y amp, phase, offset
|
||||
# 0 0 0 1 0 2 1 0 1 1 1 2
|
||||
correction_x = (
|
||||
tomo_fit_xray_eye[0][0] * np.sin(np.radians(angle) + tomo_fit_xray_eye[0][1])
|
||||
+ tomo_fit_xray_eye[0][2]
|
||||
) / 1000
|
||||
correction_y = (
|
||||
tomo_fit_xray_eye[1][0] * np.sin(np.radians(angle) + tomo_fit_xray_eye[1][1])
|
||||
+ tomo_fit_xray_eye[1][2]
|
||||
) / 1000
|
||||
|
||||
print(f"Xeye correction x {correction_x}, y {correction_y} for angle {angle}\n")
|
||||
return (correction_x, correction_y)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Additional lookup-table corrections (iteration 1 and 2)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def read_additional_correction(self, correction_file: str):
|
||||
self.corr_pos_x, self.corr_pos_y, self.corr_angle = self._read_correction_file_xy(
|
||||
correction_file
|
||||
)
|
||||
|
||||
def read_additional_correction_2(self, correction_file: str):
|
||||
self.corr_pos_x_2, self.corr_pos_y_2, self.corr_angle_2 = self._read_correction_file_xy(
|
||||
correction_file
|
||||
)
|
||||
|
||||
def _read_correction_file_xy(self, correction_file: str):
|
||||
"""Parse a correction file that contains corr_pos_x, corr_pos_y and corr_angle entries."""
|
||||
with open(correction_file, "r") as f:
|
||||
num_elements = f.readline()
|
||||
int_num_elements = int(num_elements.split(" ")[2])
|
||||
print(int_num_elements)
|
||||
corr_pos_x = []
|
||||
corr_pos_y = []
|
||||
corr_angle = []
|
||||
for j in range(0, int_num_elements * 3):
|
||||
line = f.readline()
|
||||
value = line.split(" ")[2]
|
||||
name = line.split(" ")[0].split("[")[0]
|
||||
if name == "corr_pos_x":
|
||||
corr_pos_x.append(float(value) / 1000)
|
||||
elif name == "corr_pos_y":
|
||||
corr_pos_y.append(float(value) / 1000)
|
||||
elif name == "corr_angle":
|
||||
corr_angle.append(float(value))
|
||||
return corr_pos_x, corr_pos_y, corr_angle
|
||||
|
||||
def compute_additional_correction(self, angle):
|
||||
return self._compute_correction_xy(
|
||||
angle, self.corr_pos_x, self.corr_pos_y, self.corr_angle, label="1"
|
||||
)
|
||||
|
||||
def compute_additional_correction_2(self, angle):
|
||||
return self._compute_correction_xy(
|
||||
angle, self.corr_pos_x_2, self.corr_pos_y_2, self.corr_angle_2, label="2"
|
||||
)
|
||||
|
||||
def _compute_correction_xy(self, angle, corr_pos_x, corr_pos_y, corr_angle, label=""):
|
||||
"""Find the correction for the closest angle in the lookup table."""
|
||||
if not corr_pos_x:
|
||||
print(f"Not applying additional correction {label}. No data available.\n")
|
||||
return (0, 0)
|
||||
|
||||
shift_x = corr_pos_x[0]
|
||||
shift_y = corr_pos_y[0]
|
||||
angledelta = np.fabs(corr_angle[0] - angle)
|
||||
|
||||
for j in range(1, len(corr_pos_x)):
|
||||
newangledelta = np.fabs(corr_angle[j] - angle)
|
||||
if newangledelta < angledelta:
|
||||
shift_x = corr_pos_x[j]
|
||||
shift_y = corr_pos_y[j]
|
||||
angledelta = newangledelta
|
||||
|
||||
if shift_x == 0 and angle < corr_angle[0]:
|
||||
shift_x = corr_pos_x[0]
|
||||
shift_y = corr_pos_y[0]
|
||||
|
||||
if shift_x == 0 and angle > corr_angle[-1]:
|
||||
shift_x = corr_pos_x[-1]
|
||||
shift_y = corr_pos_y[-1]
|
||||
|
||||
print(f"Additional correction shifts {label}: {shift_x} {shift_y}")
|
||||
return (shift_x, shift_y)
|
||||
@@ -1,14 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
|
||||
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
|
||||
import datetime
|
||||
import time
|
||||
|
||||
if builtins.__dict__.get("bec") is not None:
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
@@ -18,75 +19,126 @@ class LamniGuiToolsError(Exception):
|
||||
|
||||
|
||||
class LamniGuiTools:
|
||||
GUI_RPC_TIMEOUT = 20
|
||||
|
||||
def __init__(self):
|
||||
self.lamni_window = None
|
||||
self.text_box = None
|
||||
self.progressbar = None
|
||||
self.xeyegui = None
|
||||
self.pdf_viewer = None
|
||||
self.idle_text_box = None
|
||||
|
||||
def set_client(self, client):
|
||||
self.client = client
|
||||
self.gui = self.client.gui
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Window management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def lamnigui_show_gui(self):
|
||||
if "lamni" in self.gui.windows:
|
||||
self.gui.lamni.show()
|
||||
self.lamni_window = self.gui.windows["lamni"]
|
||||
self.gui.lamni.raise_window()
|
||||
else:
|
||||
self.gui.new("lamni")
|
||||
|
||||
def lamnigui_stop_gui(self):
|
||||
self.gui.lamni.hide()
|
||||
self.lamni_window = self.gui.new("lamni", timeout=self.GUI_RPC_TIMEOUT)
|
||||
time.sleep(1)
|
||||
|
||||
def lamnigui_raise(self):
|
||||
self.gui.lamni.raise_window()
|
||||
|
||||
def lamnigui_show_xeyealign(self):
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_check_attribute_not_exists("xeyegui"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
self.xeyegui = self.gui.lamni.new("xeyegui").new("XRayEye")
|
||||
# start live
|
||||
if not dev.cam_xeye.live_mode:
|
||||
dev.cam_xeye.live_mode = True
|
||||
# ------------------------------------------------------------------
|
||||
# Widget existence checks
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _lamnigui_is_missing(self, attribute_name: str) -> bool:
|
||||
"""Check whether a stored widget reference is absent or has been deleted."""
|
||||
widget = getattr(self, attribute_name, None)
|
||||
if widget is None:
|
||||
return True
|
||||
if hasattr(widget, "_is_deleted") and widget._is_deleted():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _lamnigui_check_attribute_not_exists(self, attribute_name):
|
||||
if hasattr(self.gui,"lamni"):
|
||||
if hasattr(self.gui.lamni,attribute_name):
|
||||
return False
|
||||
return True
|
||||
# ------------------------------------------------------------------
|
||||
# Dock management
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def lamnigui_remove_all_docks(self):
|
||||
self.gui.lamni.delete_all()
|
||||
if hasattr(self.gui, "lamni"):
|
||||
self.gui.lamni.delete_all(timeout=self.GUI_RPC_TIMEOUT)
|
||||
self.progressbar = None
|
||||
self.text_box = None
|
||||
self.xeyegui = None
|
||||
self.pdf_viewer = None
|
||||
self.idle_text_box = None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X-ray eye alignment views
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def lamnigui_show_xeyealign(self):
|
||||
"""Open (or raise) the X-ray eye widget on the Alignment tab."""
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_is_missing("xeyegui"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
self.xeyegui = self.gui.lamni.new(
|
||||
"XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
if not dev.cam_xeye.live_mode_enabled.get():
|
||||
dev.cam_xeye.live_mode_enabled.put(True)
|
||||
self.xeyegui.switch_tab("alignment")
|
||||
|
||||
def lamnigui_show_xeyealign_fittab(self):
|
||||
"""Open (or raise) the X-ray eye widget on the Fit tab."""
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_is_missing("xeyegui"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
self.xeyegui = self.gui.lamni.new(
|
||||
"XRayEye", object_name="xrayeye", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
self.xeyegui.switch_tab("fit")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Idle splash
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def lamnigui_idle(self):
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_check_attribute_not_exists("idle_text_box"):
|
||||
if self._lamnigui_is_missing("idle_text_box"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
idle_text_box = self.gui.lamni.new("idle_textbox").new("TextBox")
|
||||
text = (
|
||||
"<pre>"
|
||||
+ "██████╗ ███████╗ ██████╗ ██╗ █████╗ ███╗ ███╗███╗ ██╗██╗\n"
|
||||
+ "██╔══██╗██╔════╝██╔════╝ ██║ ██╔══██╗████╗ ████║████╗ ██║██║\n"
|
||||
+ "██████╔╝█████╗ ██║ ██║ ███████║██╔████╔██║██╔██╗ ██║██║\n"
|
||||
+ "██╔══██╗██╔══╝ ██║ ██║ ██╔══██║██║╚██╔╝██║██║╚██╗██║██║\n"
|
||||
+ "██████╔╝███████╗╚██████╗ ███████╗██║ ██║██║ ╚═╝ ██║██║ ╚████║██║\n"
|
||||
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝\n"
|
||||
+ "</pre>"
|
||||
)
|
||||
idle_text_box.set_html_text(text)
|
||||
self.idle_text_box = self.gui.lamni.new(
|
||||
"TextBox", object_name="idle_textbox", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
text = (
|
||||
"<pre>"
|
||||
+ "██████╗ ███████╗ ██████╗ ██╗ █████╗ ███╗ ███╗███╗ ██╗██╗\n"
|
||||
+ "██╔══██╗██╔════╝██╔════╝ ██║ ██╔══██╗████╗ ████║████╗ ██║██║\n"
|
||||
+ "██████╔╝█████╗ ██║ ██║ ███████║██╔████╔██║██╔██╗ ██║██║\n"
|
||||
+ "██╔══██╗██╔══╝ ██║ ██║ ██╔══██║██║╚██╔╝██║██║╚██╗██║██║\n"
|
||||
+ "██████╔╝███████╗╚██████╗ ███████╗██║ ██║██║ ╚═╝ ██║██║ ╚████║██║\n"
|
||||
+ "╚═════╝ ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝\n"
|
||||
+ "</pre>"
|
||||
)
|
||||
self.idle_text_box.set_html_text(text)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Documentation viewer
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def lamnigui_docs(self, filename: str | None = None):
|
||||
import csaxs_bec
|
||||
from pathlib import Path
|
||||
|
||||
print("The general lamni documentation is at \nhttps://sls-csaxs.readthedocs.io/en/latest/user/ptychography/lamni.html#user-ptychography-lamni")
|
||||
print(
|
||||
"The general LamNI documentation is at \n"
|
||||
"https://sls-csaxs.readthedocs.io/en/latest/user/ptychography/lamni.html"
|
||||
)
|
||||
|
||||
csaxs_bec_basepath = Path(csaxs_bec.__file__).parent
|
||||
docs_folder = (
|
||||
csaxs_bec_basepath /
|
||||
"bec_ipython_client" / "plugins" / "lamni" / "docs"
|
||||
csaxs_bec_basepath / "bec_ipython_client" / "plugins" / "LamNI" / "docs"
|
||||
)
|
||||
|
||||
if not docs_folder.is_dir():
|
||||
@@ -96,20 +148,19 @@ class LamniGuiTools:
|
||||
if not pdfs:
|
||||
raise FileNotFoundError(f"No PDF files found in {docs_folder}")
|
||||
|
||||
# --- Resolve PDF ------------------------------------------------------
|
||||
if filename is not None:
|
||||
pdf_file = docs_folder / filename
|
||||
if not pdf_file.exists():
|
||||
raise FileNotFoundError(f"Requested file not found: {filename}")
|
||||
else:
|
||||
print("\nAvailable lamni documentation PDFs:\n")
|
||||
print("\nAvailable LamNI documentation PDFs:\n")
|
||||
for i, pdf in enumerate(pdfs, start=1):
|
||||
print(f" {i:2d}) {pdf.name}")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = int(input(f"Select a file (1–{len(pdfs)}): "))
|
||||
choice = int(input(f"Select a file (1-{len(pdfs)}): "))
|
||||
if 1 <= choice <= len(pdfs):
|
||||
pdf_file = pdfs[choice - 1]
|
||||
break
|
||||
@@ -117,62 +168,97 @@ class LamniGuiTools:
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
|
||||
# --- GUI handling (active existence check) ----------------------------
|
||||
self.lamnigui_show_gui()
|
||||
|
||||
if self._lamnigui_check_attribute_not_exists("PdfViewerWidget"):
|
||||
if self._lamnigui_is_missing("pdf_viewer"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
self.pdf_viewer = self.gui.lamni.new(widget="PdfViewerWidget")
|
||||
self.pdf_viewer = self.gui.lamni.new(
|
||||
widget="PdfViewerWidget", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
|
||||
# --- Load PDF ---------------------------------------------------------
|
||||
self.pdf_viewer.PdfViewerWidget.load_pdf(str(pdf_file.resolve()))
|
||||
self.pdf_viewer.load_pdf(str(pdf_file.resolve()))
|
||||
print(f"\nLoaded: {pdf_file.name}\n")
|
||||
|
||||
|
||||
def _lamnicam_check_device_exists(self, device):
|
||||
try:
|
||||
device
|
||||
except:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
# ------------------------------------------------------------------
|
||||
# Progress bar
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def lamnigui_show_progress(self):
|
||||
self.lamnigui_show_gui()
|
||||
if self._lamnigui_check_attribute_not_exists("progressbar"):
|
||||
if self._lamnigui_is_missing("progressbar"):
|
||||
self.lamnigui_remove_all_docks()
|
||||
# Add a new dock with a RingProgressBar widget
|
||||
self.progressbar = self.gui.lamni.new("progressbar").new("RingProgressBar")
|
||||
# Customize the size of the progress ring
|
||||
self.progressbar.set_line_widths(20)
|
||||
# Disable automatic updates and manually set the self.progressbar value
|
||||
self.progressbar.enable_auto_updates(False)
|
||||
# Set precision for the self.progressbar display
|
||||
self.progressbar.set_precision(1) # Display self.progressbar with one decimal places
|
||||
# Setting multiple rigns with different values
|
||||
self.progressbar.set_number_of_bars(3)
|
||||
self.progressbar.rings[0].set_update("manual")
|
||||
self.progressbar.rings[1].set_update("manual")
|
||||
self.progressbar.rings[2].set_update("scan")
|
||||
# Set the values of the rings to 50, 75, and 25 from outer to inner ring
|
||||
# self.progressbar.set_value([50, 75])
|
||||
# Add a new dock with a TextBox widget
|
||||
self.text_box = self.gui.lamni.new(name="progress_text").new("TextBox")
|
||||
self.progressbar = self.gui.lamni.new(
|
||||
"RingProgressBar", timeout=self.GUI_RPC_TIMEOUT
|
||||
)
|
||||
# Outer ring: overall tomo progress (manual update)
|
||||
self.progressbar.add_ring().set_update("manual")
|
||||
# Middle ring: current sub-tomo progress (manual update)
|
||||
self.progressbar.add_ring().set_update("manual")
|
||||
# Inner ring: current scan progress (driven by BEC scan events)
|
||||
self.progressbar.add_ring().set_update("scan")
|
||||
|
||||
self._lamnigui_update_progress()
|
||||
|
||||
def _lamnigui_update_progress(self):
|
||||
if self.progressbar is not None:
|
||||
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
||||
subtomo_progress = (
|
||||
self.progress["subtomo_projection"]
|
||||
/ self.progress["subtomo_total_projections"]
|
||||
* 100
|
||||
"""Update the progress ring bar and centre label from the current progress state.
|
||||
|
||||
``self.progress`` is backed by the BEC global variable ``tomo_progress``
|
||||
so this reflects the live state accessible from any BEC client session via::
|
||||
|
||||
client.get_global_var("tomo_progress")
|
||||
"""
|
||||
if self.progressbar is None:
|
||||
return
|
||||
|
||||
main_progress_ring = self.progressbar.rings[0]
|
||||
subtomo_progress_ring = self.progressbar.rings[1]
|
||||
|
||||
progress = self.progress["projection"] / self.progress["total_projections"] * 100
|
||||
subtomo_progress = (
|
||||
self.progress["subtomo_projection"]
|
||||
/ self.progress["subtomo_total_projections"]
|
||||
* 100
|
||||
)
|
||||
main_progress_ring.set_value(progress)
|
||||
subtomo_progress_ring.set_value(subtomo_progress)
|
||||
|
||||
# Format start time
|
||||
start_str = self.progress.get("tomo_start_time")
|
||||
if start_str is not None:
|
||||
start_display = datetime.datetime.fromisoformat(start_str).strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
self.progressbar.set_value([progress, subtomo_progress, 0])
|
||||
if self.text_box is not None:
|
||||
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
|
||||
self.text_box.set_plain_text(text)
|
||||
else:
|
||||
start_display = "N/A"
|
||||
|
||||
# Format estimated remaining time
|
||||
remaining_s = self.progress.get("estimated_remaining_time")
|
||||
if remaining_s is not None and remaining_s >= 0:
|
||||
remaining_s = int(remaining_s)
|
||||
h, rem = divmod(remaining_s, 3600)
|
||||
m, s = divmod(rem, 60)
|
||||
if h > 0:
|
||||
eta_display = f"{h}h {m:02d}m {s:02d}s"
|
||||
elif m > 0:
|
||||
eta_display = f"{m}m {s:02d}s"
|
||||
else:
|
||||
eta_display = f"{s}s"
|
||||
else:
|
||||
eta_display = "N/A"
|
||||
|
||||
text = (
|
||||
f"Progress report:\n"
|
||||
f" Tomo type: {self.progress['tomo_type']}\n"
|
||||
f" Projection: {self.progress['projection']:.0f}\n"
|
||||
f" Total projections expected: {self.progress['total_projections']:.1f}\n"
|
||||
f" Angle: {self.progress['angle']:.1f}\n"
|
||||
f" Current subtomo: {self.progress['subtomo']}\n"
|
||||
f" Current projection within subtomo: {self.progress['subtomo_projection']}\n"
|
||||
f" Total projections per subtomo: {int(self.progress['subtomo_total_projections'])}\n"
|
||||
f" Scan started: {start_display}\n"
|
||||
f" Est. remaining: {eta_display}"
|
||||
)
|
||||
self.progressbar.set_center_label(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -183,6 +269,7 @@ if __name__ == "__main__":
|
||||
client.start()
|
||||
client.gui = BECGuiClient()
|
||||
|
||||
lamni_gui = LamniGuiTools(client)
|
||||
lamni_gui = LamniGuiTools()
|
||||
lamni_gui.set_client(client)
|
||||
lamni_gui.lamnigui_show_gui()
|
||||
lamni_gui.lamnigui_show_progress()
|
||||
lamni_gui.lamnigui_show_progress()
|
||||
@@ -17,8 +17,9 @@ from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import (
|
||||
TomoIDManager,
|
||||
)
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.gui_tools import LamniGuiTools
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni_alignment_mixin import LamNIAlignmentMixin
|
||||
|
||||
from .alignment import XrayEyeAlign
|
||||
from .x_ray_eye_align import XrayEyeAlign as XrayEyeAlignGUI
|
||||
from .lamni_optics_mixin import LaMNIInitStages, LamNIOpticsMixin
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -31,14 +32,22 @@ if builtins.__dict__.get("bec") is not None:
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
|
||||
class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
class LamNI(LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools):
|
||||
def __init__(self, client):
|
||||
super().__init__()
|
||||
self.client = client
|
||||
self.set_client(client)
|
||||
self.device_manager = client.device_manager
|
||||
self.align = XrayEyeAlign(client, self)
|
||||
self.init = LaMNIInitStages(client)
|
||||
|
||||
# Correction state (owned by LamNIAlignmentMixin methods)
|
||||
self.corr_pos_x = []
|
||||
self.corr_pos_y = []
|
||||
self.corr_angle = []
|
||||
self.corr_pos_x_2 = []
|
||||
self.corr_pos_y_2 = []
|
||||
self.corr_angle_2 = []
|
||||
|
||||
# Extracted collaborators
|
||||
self.reconstructor = PtychoReconstructor(self.ptycho_reconstruct_foldername)
|
||||
self.tomo_id_manager = TomoIDManager()
|
||||
@@ -60,7 +69,6 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
self.progress["total_projections"] = 1
|
||||
self.progress["angle"] = 0
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Special angles
|
||||
# ------------------------------------------------------------------
|
||||
@@ -82,6 +90,54 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
self.special_angles = []
|
||||
self.special_angle_repeats = 1
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X-ray eye alignment entry points
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def xrayeye_alignment_start(self, keep_shutter_open: bool = False):
|
||||
"""Run the BEC GUI-based X-ray eye alignment procedure.
|
||||
|
||||
Creates a fresh :class:`XrayEyeAlignGUI` instance, which resets the
|
||||
correction state, and calls its ``align()`` method. The GUI window
|
||||
is opened automatically. Interrupt with Ctrl-C to abort.
|
||||
|
||||
Args:
|
||||
keep_shutter_open: If True the shutter is left open between angle
|
||||
steps so the sample remains visible in live view.
|
||||
"""
|
||||
aligner = XrayEyeAlignGUI(self.client, self)
|
||||
try:
|
||||
aligner.align(keep_shutter_open=keep_shutter_open)
|
||||
except KeyboardInterrupt as exc:
|
||||
print("Alignment interrupted by user.")
|
||||
raise exc
|
||||
|
||||
# ── Reset manual shifts if needed ─────────────────────────────
|
||||
mx = self.manual_shift_x
|
||||
my = self.manual_shift_y
|
||||
|
||||
if mx != 0.0 or my != 0.0:
|
||||
self.manual_shift_x = 0.0
|
||||
self.manual_shift_y = 0.0
|
||||
|
||||
# Use OMNY-style green status message
|
||||
self.OMNYTools.printgreen(
|
||||
f"Manual shifts were reset to zero after X-ray eye alignment "
|
||||
f"(previous values: x={mx:.3f}, y={my:.3f})."
|
||||
)
|
||||
|
||||
def xrayeye_update_frame(self, keep_shutter_open: bool = False):
|
||||
"""Capture a single fresh X-ray eye frame without running full alignment.
|
||||
|
||||
Useful for visually checking the sample position. Opens the X-ray eye
|
||||
GUI if it is not already visible.
|
||||
|
||||
Args:
|
||||
keep_shutter_open: If True the shutter is left open after the frame.
|
||||
"""
|
||||
aligner = XrayEyeAlignGUI(self.client, self)
|
||||
aligner.update_frame(keep_shutter_open=keep_shutter_open)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# RT feedback / interferometer helpers
|
||||
# ------------------------------------------------------------------
|
||||
@@ -338,7 +394,9 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
|
||||
@golden_projections_at_0_deg_for_damage_estimation.setter
|
||||
def golden_projections_at_0_deg_for_damage_estimation(self, val: int):
|
||||
self.client.set_global_var("golden_projections_at_0_deg_for_damage_estimation", val)
|
||||
self.client.set_global_var(
|
||||
"golden_projections_at_0_deg_for_damage_estimation", val
|
||||
)
|
||||
|
||||
@property
|
||||
def sample_name(self):
|
||||
@@ -380,9 +438,7 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
)
|
||||
|
||||
def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None:
|
||||
tomo_scan_numbers_file = os.path.expanduser(
|
||||
"~/Data10/specES1/dat-files/tomography_scannumbers.txt"
|
||||
)
|
||||
tomo_scan_numbers_file = os.path.expanduser("~/data/raw/logs/tomography_scannumbers.txt")
|
||||
with open(tomo_scan_numbers_file, "a+") as out_file:
|
||||
out_file.write(
|
||||
f"{scan_number} {angle} {dev.lsamrot.read()['lsamrot']['value']:.3f}"
|
||||
@@ -390,7 +446,7 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Sample database — delegated to TomoIDManager in omny general tools
|
||||
# Sample database — delegated to TomoIDManager
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def add_sample_database(
|
||||
@@ -414,9 +470,9 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
def tomo_scan_projection(self, angle: float):
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
additional_correction = self.align.compute_additional_correction(angle)
|
||||
additional_correction_2 = self.align.compute_additional_correction_2(angle)
|
||||
correction_xeye_mu = self.align.lamni_compute_additional_correction_xeye_mu(angle)
|
||||
additional_correction = self.compute_additional_correction(angle)
|
||||
additional_correction_2 = self.compute_additional_correction_2(angle)
|
||||
correction_xeye_mu = self.lamni_compute_additional_correction_xeye_mu(angle)
|
||||
|
||||
self._current_scan_list = []
|
||||
|
||||
@@ -434,8 +490,8 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
stitch_x=stitch_x,
|
||||
stitch_y=stitch_y,
|
||||
stitch_overlap=self.tomo_stitch_overlap,
|
||||
center_x=self.align.tomo_fovx_offset,
|
||||
center_y=self.align.tomo_fovy_offset,
|
||||
center_x=self.tomo_fovx_offset,
|
||||
center_y=self.tomo_fovy_offset,
|
||||
shift_x=(
|
||||
self.manual_shift_x
|
||||
+ correction_xeye_mu[0]
|
||||
@@ -455,8 +511,8 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
optim_trajectory_corridor=corridor_size,
|
||||
)
|
||||
|
||||
def tomo_reconstruct(self, base_path="~/Data10/specES1"):
|
||||
"""Write the tomo reconstruct file for the reconstruction queue."""
|
||||
def tomo_reconstruct(self, base_path="~/data/raw/logs/reconstruction_queue"):
|
||||
"""write the tomo reconstruct file for the reconstruction queue"""
|
||||
bec = builtins.__dict__.get("bec")
|
||||
self.reconstructor.write(
|
||||
scan_list=self._current_scan_list,
|
||||
@@ -480,6 +536,7 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
print(f"Angle: ........................... {self.progress['angle']}")
|
||||
print(f"Current subtomo: ................. {self.progress['subtomo']}")
|
||||
print(f"Current projection within subtomo: {self.progress['subtomo_projection']}\x1b[0m")
|
||||
self._lamnigui_update_progress()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Tomo scan orchestration
|
||||
@@ -550,10 +607,8 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
for scan_nr in range(start_scan_number, end_scan_number):
|
||||
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
|
||||
|
||||
#todo here bl chk, if ok then successfull true
|
||||
successful = True
|
||||
|
||||
|
||||
def _golden(self, ii, howmany_sorted, maxangle=360, reverse=False):
|
||||
"""Return the ii-th golden ratio angle within sorted bunches and its subtomo number."""
|
||||
golden = []
|
||||
@@ -733,11 +788,12 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
print(f"Stitching overlap = {self.tomo_stitch_overlap}")
|
||||
print(f"Circular FOV diam <microns> = {self.tomo_circfov}")
|
||||
print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}")
|
||||
print("FOV offset rotates to find the ROI; manual shift moves the rotation center.")
|
||||
print(f" _tomo_fovx_offset <mm> = {self.align.tomo_fovx_offset}")
|
||||
print(f" _tomo_fovy_offset <mm> = {self.align.tomo_fovy_offset}")
|
||||
print(f" _manual_shift_x <mm> = {self.manual_shift_x}")
|
||||
print(f" _manual_shift_y <mm> = {self.manual_shift_y}")
|
||||
print("FOV offset rotates to find the ROI; initial values determined in Xrayeye alignment.")
|
||||
print("manual shift moves the rotation center.")
|
||||
print(f" _tomo_fovx_offset <mm> = {self.tomo_fovx_offset:.4f}")
|
||||
print(f" _tomo_fovy_offset <mm> = {self.tomo_fovy_offset:.4f}")
|
||||
print(f" _manual_shift_x <mm> = {self.manual_shift_x:.4f}")
|
||||
print(f" _manual_shift_y <mm> = {self.manual_shift_y:.4f}")
|
||||
print("")
|
||||
if self.tomo_type == 1:
|
||||
print("\x1b[1mTomo type 1:\x1b[0m 8 equally spaced sub-tomograms (360 deg)")
|
||||
@@ -764,8 +820,8 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
print("Repeating projections at 0 deg at start of every second subtomogram.")
|
||||
print(f"\nSample name: {self.sample_name}\n")
|
||||
|
||||
user_input = input("Are these parameters correctly set for your scan? ")
|
||||
if user_input == "y":
|
||||
|
||||
if self.OMNYTools.yesno("Are these parameters correctly set for your scan?", "y"):
|
||||
print("OK. continue.")
|
||||
return
|
||||
|
||||
@@ -898,7 +954,6 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
)
|
||||
self.client.logbook.send_logbook_message(msg)
|
||||
|
||||
|
||||
def get_calibration_of_capstops_left_and_right(self):
|
||||
import time
|
||||
print("""
|
||||
@@ -909,28 +964,28 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
|
||||
Example: At 0 deg, accessible rty -60 to 51. So the init was 5 microns off.
|
||||
Then this routine here will provide data for the new capstop left and right.
|
||||
""")
|
||||
|
||||
|
||||
angle = 0
|
||||
umv(dev.lsamrot,0)
|
||||
umv(dev.lsamrot, 0)
|
||||
print("Capstop right\nAngle, Voltage1, Voltage2")
|
||||
mv(dev.lsamrot,361)
|
||||
mv(dev.lsamrot, 361)
|
||||
while angle <= 360:
|
||||
angle = dev.lsamrot.readback.get()
|
||||
voltage1=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[1]"))
|
||||
voltage2=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[2]"))
|
||||
if angle<360:
|
||||
voltage1 = float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[1]"))
|
||||
voltage2 = float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[2]"))
|
||||
if angle < 360:
|
||||
print(f"{angle},{voltage1},{voltage2}")
|
||||
time.sleep(.3)
|
||||
|
||||
time.sleep(10)
|
||||
print("\nCapstop left\nAngle, Voltage1, Voltage2")
|
||||
mv(dev.lsamrot,-1)
|
||||
mv(dev.lsamrot, -1)
|
||||
while angle > 0:
|
||||
angle = dev.lsamrot.readback.get()
|
||||
voltage1=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[1]"))
|
||||
voltage2=float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[2]"))
|
||||
if angle>0:
|
||||
voltage1 = float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[1]"))
|
||||
voltage2 = float(dev.lsamrot.controller.socket_put_and_receive("MG@AN[2]"))
|
||||
if angle > 0:
|
||||
print(f"{angle},{voltage1},{voltage2}")
|
||||
time.sleep(.3)
|
||||
|
||||
print("Finished")
|
||||
print("Finished")
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
"""LamNI alignment correction infrastructure.
|
||||
|
||||
This mixin provides the correction infrastructure that is mixed directly into
|
||||
the :class:`LamNI` class, mirroring the pattern used by
|
||||
:class:`FlomniAlignmentMixin` in flomni.py.
|
||||
|
||||
The mixin assumes the hosting class provides:
|
||||
- ``self.client`` (BECClient)
|
||||
|
||||
State that must be initialised in the host ``__init__``:
|
||||
- ``self.corr_pos_x``, ``self.corr_pos_y``, ``self.corr_angle``
|
||||
- ``self.corr_pos_x_2``, ``self.corr_pos_y_2``, ``self.corr_angle_2``
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
from bec_lib import bec_logger
|
||||
from typeguard import typechecked
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
dev = builtins.__dict__.get("dev")
|
||||
|
||||
|
||||
class LamNIAlignmentMixin:
|
||||
"""Correction infrastructure for LamNI laminography.
|
||||
|
||||
Mixes into :class:`LamNI`. All methods are accessible directly as
|
||||
``lamni.reset_correction()``, ``lamni.tomo_fovx_offset``, etc.,
|
||||
matching the FlOMNI user API.
|
||||
"""
|
||||
|
||||
# Pixel calibration: mm per pixel (0.2 mm FOV, 218 px)
|
||||
PIXEL_CALIBRATION = 0.2 / 218
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Correction reset
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def reset_correction(self):
|
||||
"""Reset the look-up-table corrections to empty (iteration 1 and 2)."""
|
||||
self.corr_pos_x = []
|
||||
self.corr_pos_y = []
|
||||
self.corr_angle = []
|
||||
|
||||
def reset_correction_2(self):
|
||||
"""Reset the second iteration look-up-table correction to empty."""
|
||||
self.corr_pos_x_2 = []
|
||||
self.corr_pos_y_2 = []
|
||||
self.corr_angle_2 = []
|
||||
|
||||
def reset_xray_eye_correction(self):
|
||||
"""Delete the X-ray eye sinusoidal fit from the BEC global variable store."""
|
||||
self.client.delete_global_var("tomo_fit_xray_eye")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# FOV offset properties (backed by BEC global variable)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def tomo_fovx_offset(self):
|
||||
"""Horizontal FOV offset in mm (rotated with laminography geometry)."""
|
||||
val = self.client.get_global_var("tomo_fov_offset")
|
||||
if val is None:
|
||||
return 0.0
|
||||
return val[0] / 1000
|
||||
|
||||
@tomo_fovx_offset.setter
|
||||
@typechecked
|
||||
def tomo_fovx_offset(self, val: float):
|
||||
val_old = self.client.get_global_var("tomo_fov_offset")
|
||||
if val_old is None:
|
||||
val_old = [0.0, 0.0]
|
||||
self.client.set_global_var("tomo_fov_offset", [val * 1000, val_old[1]])
|
||||
|
||||
@property
|
||||
def tomo_fovy_offset(self):
|
||||
"""Vertical FOV offset in mm (rotated with laminography geometry)."""
|
||||
val = self.client.get_global_var("tomo_fov_offset")
|
||||
if val is None:
|
||||
return 0.0
|
||||
return val[1] / 1000
|
||||
|
||||
@tomo_fovy_offset.setter
|
||||
@typechecked
|
||||
def tomo_fovy_offset(self, val: float):
|
||||
val_old = self.client.get_global_var("tomo_fov_offset")
|
||||
if val_old is None:
|
||||
val_old = [0.0, 0.0]
|
||||
self.client.set_global_var("tomo_fov_offset", [val_old[0], val * 1000])
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# X-ray eye sinusoidal correction — read from files or GUI
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
# def read_xray_eye_correction(self, dir_path=None):
|
||||
# """Load the sinusoidal X-ray eye fit from archived text files.
|
||||
|
||||
# Files are written by :meth:`XrayEyeAlign.write_output` at the end of
|
||||
# every alignment run and are the fallback when the GUI has been closed.
|
||||
|
||||
# Args:
|
||||
# dir_path: Directory containing ``ptychotomoalign_{A,B,C}{x,y}.txt``.
|
||||
# Defaults to ``~/Data10/specES1/internal/``.
|
||||
# """
|
||||
# if dir_path is None:
|
||||
# dir_path = os.path.expanduser("~/Data10/specES1/internal/")
|
||||
# tomo_fit_xray_eye = np.zeros((2, 3))
|
||||
# for i, axis in enumerate(["x", "y"]):
|
||||
# for j, coeff in enumerate(["A", "B", "C"]):
|
||||
# with open(
|
||||
# os.path.join(dir_path, f"ptychotomoalign_{coeff}{axis}.txt"), "r"
|
||||
# ) as f:
|
||||
# tomo_fit_xray_eye[i][j] = f.readline()
|
||||
# self.client.set_global_var("tomo_fit_xray_eye", tomo_fit_xray_eye.tolist())
|
||||
# print("New alignment parameters loaded from X-ray eye files:")
|
||||
# self._print_xeye_fit(tomo_fit_xray_eye)
|
||||
|
||||
def read_xray_eye_correction_from_gui(self):
|
||||
"""Load the sinusoidal X-ray eye fit from the live XRayEye GUI widget.
|
||||
|
||||
Reads ``fit_params_x`` and ``fit_params_y`` from the ``omny_xray_gui``
|
||||
device and stores the result as
|
||||
``tomo_fit_xray_eye = [[Ax, Bx, Cx], [Ay, By, Cy]]``
|
||||
in the BEC global variable store.
|
||||
|
||||
The stored array is consumed by
|
||||
:meth:`lamni_compute_additional_correction_xeye_mu`.
|
||||
|
||||
.. important::
|
||||
This method reads from the live GUI widget. If the XRayEye GUI
|
||||
window has been closed since the alignment was performed the call
|
||||
will fail. In that case use :meth:`read_xray_eye_correction` to
|
||||
reload from the archived text files.
|
||||
"""
|
||||
_dev = builtins.__dict__.get("dev")
|
||||
tomo_fit_xray_eye = np.zeros((2, 3))
|
||||
|
||||
params_x = _dev.omny_xray_gui.fit_params_x.get()
|
||||
tomo_fit_xray_eye[0][0] = params_x["SineModel_0_amplitude"]
|
||||
tomo_fit_xray_eye[0][1] = params_x["SineModel_0_shift"]
|
||||
tomo_fit_xray_eye[0][2] = params_x["LinearModel_1_intercept"]
|
||||
|
||||
params_y = _dev.omny_xray_gui.fit_params_y.get()
|
||||
tomo_fit_xray_eye[1][0] = params_y["SineModel_0_amplitude"]
|
||||
tomo_fit_xray_eye[1][1] = params_y["SineModel_0_shift"]
|
||||
tomo_fit_xray_eye[1][2] = params_y["LinearModel_1_intercept"]
|
||||
|
||||
self.client.set_global_var("tomo_fit_xray_eye", tomo_fit_xray_eye.tolist())
|
||||
print("New alignment parameters loaded from XRayEye GUI fit:")
|
||||
self._print_xeye_fit(tomo_fit_xray_eye)
|
||||
|
||||
@staticmethod
|
||||
def _print_xeye_fit(fit):
|
||||
"""Pretty-print the 2×3 X-ray eye fit array."""
|
||||
print(
|
||||
f" X: A={fit[0][0]:.4f}, B={fit[0][1]:.4f}, C={fit[0][2]:.4f}\n"
|
||||
f" Y: A={fit[1][0]:.4f}, B={fit[1][1]:.4f}, C={fit[1][2]:.4f}"
|
||||
)
|
||||
|
||||
def lamni_compute_additional_correction_xeye_mu(self, angle):
|
||||
"""Evaluate the sinusoidal X-ray eye correction at *angle* degrees.
|
||||
|
||||
Returns:
|
||||
tuple: ``(correction_x_mm, correction_y_mm)``
|
||||
"""
|
||||
tomo_fit_xray_eye = self.client.get_global_var("tomo_fit_xray_eye")
|
||||
if tomo_fit_xray_eye is None:
|
||||
print("Not applying any X-ray eye correction. No fit data available.\n")
|
||||
return (0, 0)
|
||||
|
||||
correction_x = (
|
||||
tomo_fit_xray_eye[0][0] * np.sin(
|
||||
np.radians(angle) + tomo_fit_xray_eye[0][1]
|
||||
)
|
||||
+ tomo_fit_xray_eye[0][2]
|
||||
) / 1000
|
||||
correction_y = (
|
||||
tomo_fit_xray_eye[1][0] * np.sin(
|
||||
np.radians(angle) + tomo_fit_xray_eye[1][1]
|
||||
)
|
||||
+ tomo_fit_xray_eye[1][2]
|
||||
) / 1000
|
||||
|
||||
print(
|
||||
f"Xeye correction x={correction_x:.6f} mm,"
|
||||
f" y={correction_y:.6f} mm @ angle={angle}\n"
|
||||
)
|
||||
return (correction_x, correction_y)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Additional look-up-table corrections (iteration 1 and 2)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def read_additional_correction(self, correction_file: str):
|
||||
"""Load the iteration-1 correction lookup table from *correction_file*."""
|
||||
self.corr_pos_x, self.corr_pos_y, self.corr_angle = self._read_correction_file_xy(
|
||||
correction_file
|
||||
)
|
||||
|
||||
def read_additional_correction_2(self, correction_file: str):
|
||||
"""Load the iteration-2 correction lookup table from *correction_file*."""
|
||||
self.corr_pos_x_2, self.corr_pos_y_2, self.corr_angle_2 = (
|
||||
self._read_correction_file_xy(correction_file)
|
||||
)
|
||||
|
||||
def _read_correction_file_xy(self, correction_file: str):
|
||||
"""Parse a correction file containing ``corr_pos_x``, ``corr_pos_y``
|
||||
and ``corr_angle`` entries.
|
||||
|
||||
Returns:
|
||||
tuple: ``(corr_pos_x, corr_pos_y, corr_angle)`` as lists of floats.
|
||||
"""
|
||||
with open(correction_file, "r") as f:
|
||||
num_elements = f.readline()
|
||||
int_num_elements = int(num_elements.split(" ")[2])
|
||||
print(int_num_elements)
|
||||
corr_pos_x = []
|
||||
corr_pos_y = []
|
||||
corr_angle = []
|
||||
for _ in range(int_num_elements * 3):
|
||||
line = f.readline()
|
||||
value = line.split(" ")[2]
|
||||
name = line.split(" ")[0].split("[")[0]
|
||||
if name == "corr_pos_x":
|
||||
corr_pos_x.append(float(value) / 1000)
|
||||
elif name == "corr_pos_y":
|
||||
corr_pos_y.append(float(value) / 1000)
|
||||
elif name == "corr_angle":
|
||||
corr_angle.append(float(value))
|
||||
return corr_pos_x, corr_pos_y, corr_angle
|
||||
|
||||
def compute_additional_correction(self, angle):
|
||||
"""Return the iteration-1 lookup-table correction for *angle*.
|
||||
|
||||
Returns:
|
||||
tuple: ``(shift_x_mm, shift_y_mm)``
|
||||
"""
|
||||
return self._compute_correction_xy(
|
||||
angle, self.corr_pos_x, self.corr_pos_y, self.corr_angle, label="1"
|
||||
)
|
||||
|
||||
def compute_additional_correction_2(self, angle):
|
||||
"""Return the iteration-2 lookup-table correction for *angle*.
|
||||
|
||||
Returns:
|
||||
tuple: ``(shift_x_mm, shift_y_mm)``
|
||||
"""
|
||||
return self._compute_correction_xy(
|
||||
angle, self.corr_pos_x_2, self.corr_pos_y_2, self.corr_angle_2, label="2"
|
||||
)
|
||||
|
||||
def _compute_correction_xy(self, angle, corr_pos_x, corr_pos_y, corr_angle, label=""):
|
||||
"""Find the correction for the closest angle in the lookup table."""
|
||||
if not corr_pos_x:
|
||||
print(f"Not applying additional correction {label}. No data available.\n")
|
||||
return (0, 0)
|
||||
|
||||
shift_x = corr_pos_x[0]
|
||||
shift_y = corr_pos_y[0]
|
||||
angledelta = np.fabs(corr_angle[0] - angle)
|
||||
|
||||
for j in range(1, len(corr_pos_x)):
|
||||
newangledelta = np.fabs(corr_angle[j] - angle)
|
||||
if newangledelta < angledelta:
|
||||
shift_x = corr_pos_x[j]
|
||||
shift_y = corr_pos_y[j]
|
||||
angledelta = newangledelta
|
||||
|
||||
if shift_x == 0 and angle < corr_angle[0]:
|
||||
shift_x = corr_pos_x[0]
|
||||
shift_y = corr_pos_y[0]
|
||||
|
||||
if shift_x == 0 and angle > corr_angle[-1]:
|
||||
shift_x = corr_pos_x[-1]
|
||||
shift_y = corr_pos_y[-1]
|
||||
|
||||
print(f"Additional correction {label}: x={shift_x}, y={shift_y}")
|
||||
return (shift_x, shift_y)
|
||||
@@ -144,6 +144,8 @@ class LaMNIInitStages:
|
||||
umv(dev.lsamrot, -1)
|
||||
umv(dev.lsamrot, 0)
|
||||
|
||||
self.set_default_lamni_limits()
|
||||
|
||||
time.sleep(2)
|
||||
dev.rtx.controller.feedback_disable_and_even_reset_lamni_angle_interferometer()
|
||||
|
||||
@@ -159,6 +161,38 @@ class LaMNIInitStages:
|
||||
else:
|
||||
return False
|
||||
|
||||
def set_default_lamni_limits(self):
|
||||
"""
|
||||
Apply safe, collision-protected default limits for LamNI.
|
||||
Mirrors legacy SPEC limits.
|
||||
"""
|
||||
if not self.OMNYTools.yesno("Set default limits for LamNI?"):
|
||||
print("Stopping.")
|
||||
return
|
||||
|
||||
print("Setting LamNI limits...")
|
||||
|
||||
# Sample stages
|
||||
dev.lsamx.limits = [6, 14]
|
||||
dev.lsamy.limits = [6, 14]
|
||||
dev.lsamrot.limits = [-3, 362]
|
||||
|
||||
# Optics (FZP)
|
||||
dev.loptx.limits = [-1, -0.2]
|
||||
dev.lopty.limits = [3.0, 3.6]
|
||||
dev.loptz.limits = [82, 87]
|
||||
|
||||
# X-ray eye
|
||||
dev.leyex.limits = [0, 25]
|
||||
dev.leyey.limits = [0.5, 50]
|
||||
|
||||
# OSA / SmarAct
|
||||
dev.losax.limits = [-1.5, 0.25]
|
||||
dev.losay.limits = [-2.5, 4.1]
|
||||
dev.losaz.limits = [-4.1, -0.5]
|
||||
|
||||
print("LamNI limits successfully applied.")
|
||||
|
||||
|
||||
class LamNIOpticsMixin:
|
||||
"""Optics movement methods: FZP, OSA, central stop and X-ray eye."""
|
||||
@@ -178,12 +212,12 @@ class LamNIOpticsMixin:
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
umv(dev.lsamrot, 0)
|
||||
umv(dev.dttrz, 5854, dev.fttrz, 2395)
|
||||
#umv(dev.dttrz, 5854, dev.fttrz, 2395)
|
||||
|
||||
def leye_in(self):
|
||||
bec.queue.next_dataset_number += 1
|
||||
umv(dev.lsamrot, 0)
|
||||
umv(dev.dttrz, 6419.677, dev.fttrz, 2959.979)
|
||||
#umv(dev.dttrz, 6419.677, dev.fttrz, 2959.979)
|
||||
while True:
|
||||
moved_out = (input("Did the flight tube move out? (Y/n)") or "y").lower()
|
||||
if moved_out == "y":
|
||||
@@ -193,7 +227,7 @@ class LamNIOpticsMixin:
|
||||
leyex_in = self._get_user_param_safe("leyex", "in")
|
||||
leyey_in = self._get_user_param_safe("leyey", "in")
|
||||
umv(dev.leyex, leyex_in, dev.leyey, leyey_in)
|
||||
self.align.update_frame()
|
||||
self.xrayeye_update_frame()
|
||||
|
||||
def _lfzp_in(self):
|
||||
loptx_in = self._get_user_param_safe("loptx", "in")
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import builtins
|
||||
import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni import LamNI
|
||||
|
||||
|
||||
# Laminography alignment angles: full 360° in 45° steps
|
||||
LAMNI_ALIGNMENT_ANGLES = [k * 45 for k in range(8)] # [0, 45, 90, 135, 180, 225, 270, 315]
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
"""BEC-GUI-based X-ray eye alignment for LamNI.
|
||||
|
||||
Replaces the old EPICS/LabView interface. Collects sample centre
|
||||
coordinates (x and y) at 8 equally-spaced laminography angles over the
|
||||
full 360°, submits them to the XRayEye widget's fit tab, and then reads
|
||||
the resulting sinusoidal fit parameters back via
|
||||
``lamni.read_xray_eye_correction_from_gui()``.
|
||||
|
||||
The key difference from the FlOMNI alignment is that LamNI needs *both*
|
||||
x and y fits because the tilted rotation axis (61°) couples both
|
||||
directions.
|
||||
"""
|
||||
|
||||
# Pixel calibration: multiply pixel coordinate by this to get mm
|
||||
PIXEL_CALIBRATION = 0.2 / 218 # mm/pixel (0.2 mm FOV, 218 px)
|
||||
|
||||
def __init__(self, client, lamni: LamNI) -> None:
|
||||
self.client = client
|
||||
self.lamni = lamni
|
||||
self.device_manager = client.device_manager
|
||||
self.scans = client.scans
|
||||
# Reset correction state on the lamni object (mirrors FlOMNI pattern)
|
||||
self.lamni.reset_correction()
|
||||
self.lamni.reset_xray_eye_correction()
|
||||
# alignment_values[k] = [x_mm, y_mm]
|
||||
# k=0 : FZP centre
|
||||
# k=1 : sample at 0° (reference; shift_xy computed from k=0 vs k=1)
|
||||
# k=2 : sample at 45°
|
||||
# ...
|
||||
# k=8 : sample at 315°
|
||||
self.alignment_values: dict[int, list[float]] = {}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# GUI shortcut
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def gui(self):
|
||||
"""Return the live XRayEye RPC handle stored by LamniGuiTools."""
|
||||
return self.lamni.xeyegui
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0.0, 0.0] # base shift to bring sample to beam centre [µm]
|
||||
self._xray_fov_xy = [0.0, 0.0]
|
||||
|
||||
def tomo_rotate(self, val: float):
|
||||
umv(self.device_manager.devices.lsamrot, val)
|
||||
|
||||
def get_tomo_angle(self) -> float:
|
||||
return self.device_manager.devices.lsamrot.readback.read()["lsamrot"]["value"]
|
||||
|
||||
def _disable_rt_feedback(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_disable()
|
||||
|
||||
def _enable_rt_feedback(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_enable_with_reset()
|
||||
|
||||
def update_frame(self, keep_shutter_open: bool = False):
|
||||
"""Capture a fresh camera frame.
|
||||
|
||||
Args:
|
||||
keep_shutter_open: If True the shutter is left open and the GUI
|
||||
stays in live-view mode after the frame is taken. Useful when
|
||||
it is hard to locate the sample between rotations. The caller
|
||||
is responsible for closing the shutter afterwards.
|
||||
"""
|
||||
if not dev.cam_xeye.live_mode_enabled.get():
|
||||
dev.cam_xeye.live_mode_enabled.put(True)
|
||||
self.gui.on_live_view_enabled(True)
|
||||
dev.omnyfsh.fshopen()
|
||||
time.sleep(0.5)
|
||||
if not keep_shutter_open:
|
||||
self.gui.on_live_view_enabled(False)
|
||||
time.sleep(0.1)
|
||||
dev.omnyfsh.fshclose()
|
||||
print("Received new frame.")
|
||||
else:
|
||||
print("Received new frame. Shutter remains open and live view active.")
|
||||
|
||||
def update_fov(self, k: int):
|
||||
self._xray_fov_xy[0] = max(
|
||||
getattr(dev.omny_xray_gui, f"width_x_{k}").get(), self._xray_fov_xy[0]
|
||||
)
|
||||
self._xray_fov_xy[1] = max(
|
||||
getattr(dev.omny_xray_gui, f"width_y_{k}").get(), self._xray_fov_xy[1]
|
||||
)
|
||||
|
||||
def movement_buttons_enabled(self, enable_x: bool, enable_y: bool):
|
||||
self.gui.on_motors_enable(enable_x, enable_y)
|
||||
|
||||
def send_message(self, msg: str):
|
||||
print(f"Alignment GUI: {msg}")
|
||||
self.gui.user_message = msg
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Main alignment procedure
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def align(self, keep_shutter_open: bool = False):
|
||||
"""Run the full LamNI X-ray eye alignment.
|
||||
|
||||
Args:
|
||||
keep_shutter_open: If True the shutter is left open between angle
|
||||
steps so the sample remains visible in live view. At the end
|
||||
of the procedure the user is asked whether to close the shutter.
|
||||
Matches the equivalent FlOMNI option.
|
||||
|
||||
Procedure
|
||||
---------
|
||||
Step 0 – FZP centre
|
||||
Put FZP in, capture frame, user clicks FZP centre.
|
||||
|
||||
Step 1 – Sample at 0°
|
||||
Put sample in, capture frame, user clicks sample centre.
|
||||
Compute base shift (shift_xy) and move to scan centre.
|
||||
|
||||
Steps 2–8 – Sample at 45°, 90°, …, 315°
|
||||
Rotate to each angle (via lamni_move_to_scan_center so the base
|
||||
shift is applied), capture frame, user clicks sample centre.
|
||||
|
||||
After all submits
|
||||
Compute x/y offsets, push to GUI fit tab, wait for DAP fit,
|
||||
then load fit parameters into the global variable store.
|
||||
"""
|
||||
self.lamni.lamnigui_show_xeyealign()
|
||||
self.send_message("Getting things ready. Please wait...")
|
||||
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
# Initialise EPICS GUI device state
|
||||
dev.omny_xray_gui.mvx.set(0)
|
||||
dev.omny_xray_gui.mvy.set(0)
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self._reset_init_values()
|
||||
|
||||
# --- Step 0: FZP centre ------------------------------------------
|
||||
#self._disable_rt_feedback()
|
||||
self.lamni.lfzp_in()
|
||||
#self._enable_rt_feedback()
|
||||
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
self.gui.enable_submit_button(True)
|
||||
dev.omny_xray_gui.step.set(0)
|
||||
self.send_message("Submit centre of FZP.")
|
||||
|
||||
k = 0
|
||||
while True:
|
||||
if dev.omny_xray_gui.submit.get() == 1:
|
||||
val_x = (
|
||||
getattr(dev.omny_xray_gui, f"xval_x_{k}").get()
|
||||
* self.PIXEL_CALIBRATION
|
||||
)
|
||||
val_y = (
|
||||
getattr(dev.omny_xray_gui, f"yval_y_{k}").get()
|
||||
* self.PIXEL_CALIBRATION
|
||||
)
|
||||
self.alignment_values[k] = [val_x, val_y]
|
||||
print(
|
||||
f"Clicked position {k}: "
|
||||
f"x={self.alignment_values[k][0]:.4f} mm, "
|
||||
f"y={self.alignment_values[k][1]:.4f} mm"
|
||||
)
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
|
||||
# --- k=0: received FZP centre ----------------------------
|
||||
if k == 0:
|
||||
self.send_message("Please wait - moving sample in...")
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
self.lamni.loptics_out()
|
||||
#self._disable_rt_feedback()
|
||||
#time.sleep(0.3)
|
||||
#self._enable_rt_feedback()
|
||||
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message("Find the sample and submit its centre.")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, True)
|
||||
|
||||
# --- k=1: sample at 0° - compute base shift --------------
|
||||
elif k == 1:
|
||||
# Displacement from FZP centre to sample centre gives the
|
||||
# correction needed to bring the sample onto the beam axis.
|
||||
# Sign conventions match the old alignment.py.
|
||||
self.shift_xy[0] += (
|
||||
self.alignment_values[0][0] - self.alignment_values[1][0]
|
||||
) * 1000 # um
|
||||
self.shift_xy[1] += (
|
||||
self.alignment_values[1][1] - self.alignment_values[0][1]
|
||||
) * 1000 # um
|
||||
print(
|
||||
f"Base shift: "
|
||||
f"x={self.shift_xy[0]:.2f} um, "
|
||||
f"y={self.shift_xy[1]:.2f} um"
|
||||
)
|
||||
|
||||
self.send_message("Please wait - moving to scan centre...")
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
shift_x=self.shift_xy[0] / 1000,
|
||||
shift_y=self.shift_xy[1] / 1000,
|
||||
angle=float(self.get_tomo_angle()),
|
||||
).wait()
|
||||
time.sleep(1)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
shift_x=self.shift_xy[0] / 1000,
|
||||
shift_y=self.shift_xy[1] / 1000,
|
||||
angle=float(self.get_tomo_angle()),
|
||||
).wait()
|
||||
|
||||
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message("Submit sample centre and FOV (0 deg).")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, False)
|
||||
self.update_fov(k)
|
||||
|
||||
# --- k=2..8: sample at 45, 90, ..., 315 deg -------------
|
||||
elif 1 < k <= 8:
|
||||
self.send_message("Please wait - rotating...")
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
|
||||
target_angle = LAMNI_ALIGNMENT_ANGLES[k - 1] # 45...315 deg
|
||||
# Approach from halfway between previous and current angle
|
||||
# to reduce mechanical hysteresis (same as old alignment.py)
|
||||
prev_angle = LAMNI_ALIGNMENT_ANGLES[k - 2]
|
||||
approach_angle = prev_angle + (target_angle - prev_angle) / 2
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(approach_angle)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
shift_x=self.shift_xy[0] / 1000,
|
||||
shift_y=self.shift_xy[1] / 1000,
|
||||
angle=float(self.get_tomo_angle()),
|
||||
).wait()
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(target_angle)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
shift_x=self.shift_xy[0] / 1000,
|
||||
shift_y=self.shift_xy[1] / 1000,
|
||||
angle=float(self.get_tomo_angle()),
|
||||
).wait()
|
||||
|
||||
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message(f"Submit sample centre ({target_angle} deg).")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, False)
|
||||
self.update_fov(k)
|
||||
|
||||
# --- k=9: all angles collected - finish ------------------
|
||||
elif k == 9:
|
||||
self.send_message("All angles collected - computing fit...")
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.update_fov(k)
|
||||
break
|
||||
|
||||
k += 1
|
||||
dev.omny_xray_gui.step.set(k)
|
||||
|
||||
# Handle live motor movement buttons.
|
||||
# Only active during k=0 (FZP) and k=1 (sample at 0 deg) – both
|
||||
# x and y movements accumulate into shift_xy and are applied via
|
||||
# lamni_move_to_scan_center so the laminography coordinate
|
||||
# transformation is correctly applied. This matches the old
|
||||
# alignment.py exactly.
|
||||
if k < 2:
|
||||
_mvx = dev.omny_xray_gui.mvx.get()
|
||||
_mvy = dev.omny_xray_gui.mvy.get()
|
||||
if _mvx != 0 or _mvy != 0:
|
||||
self.shift_xy[0] += _mvx
|
||||
self.shift_xy[1] += _mvy
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
shift_x=self.shift_xy[0] / 1000,
|
||||
shift_y=self.shift_xy[1] / 1000,
|
||||
angle=float(self.get_tomo_angle()),
|
||||
).wait()
|
||||
print(
|
||||
f"Current centre: "
|
||||
f"horizontal={self.shift_xy[0]:.2f} um, "
|
||||
f"vertical={self.shift_xy[1]:.2f} um"
|
||||
)
|
||||
dev.omny_xray_gui.mvx.set(0)
|
||||
dev.omny_xray_gui.mvy.set(0)
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# All submits received - compute offsets and push to GUI fit tab
|
||||
# ------------------------------------------------------------------
|
||||
self.write_output()
|
||||
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
print(
|
||||
f"Largest FOV from X-ray eye alignment:\n"
|
||||
f" fovx = {fovx:.0f} um, fovy = {fovy:.0f} um"
|
||||
)
|
||||
print(
|
||||
f"Base shift (applied to all scan centres):\n"
|
||||
f" x = {self.shift_xy[0]:.2f} um, y = {self.shift_xy[1]:.2f} um"
|
||||
)
|
||||
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
|
||||
|
||||
# Switch GUI to fit tab and wait for DAP to finish fitting
|
||||
self.lamni.lamnigui_show_xeyealign_fittab()
|
||||
print("Waiting 5 s for DAP sinusoidal fit to converge...")
|
||||
time.sleep(5)
|
||||
|
||||
# Read fit parameters from the GUI into the global variable store
|
||||
print("Loading new alignment parameters from X-ray eye GUI fit.")
|
||||
self.lamni.read_xray_eye_correction_from_gui()
|
||||
|
||||
if keep_shutter_open:
|
||||
answer = input("Close the shutter now? [Y/n]: ").strip().lower()
|
||||
if answer in ("", "y", "yes"):
|
||||
dev.omnyfsh.fshclose()
|
||||
self.gui.on_live_view_enabled(False)
|
||||
print("Shutter closed.")
|
||||
else:
|
||||
print("Shutter left open.")
|
||||
|
||||
# Return to 0 deg
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(0)
|
||||
print(
|
||||
"Done. You are ready to remove the X-ray eye and start ptychography scans.\n"
|
||||
"Fine alignment: lamni.tomo_parameters() with offset 0, then lamni.sub_tomo_scan(1,0)"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fit data preparation and submission
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def write_output(self):
|
||||
"""Compute x/y offsets for each angle and push them to the GUI fit tab.
|
||||
|
||||
Offsets are the displacement of the sample from its position at 0 deg
|
||||
(k=1 is the reference):
|
||||
x_offset[i] = (x_at_0deg - x_at_angle_i) * 1000 [um]
|
||||
y_offset[i] = (y_at_angle_i - y_at_0deg) * 1000 [um]
|
||||
|
||||
The array passed to submit_fit_array has shape (3, 8):
|
||||
row 0: angles [0, 45, ..., 315]
|
||||
row 1: x offsets [um]
|
||||
row 2: y offsets [um]
|
||||
"""
|
||||
# Archival text file (backward compatible with any external scripts)
|
||||
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, "w") as f:
|
||||
f.write("angle\thorizontal\tvertical\n")
|
||||
for k in range(1, 9):
|
||||
angle_deg = LAMNI_ALIGNMENT_ANGLES[k - 1]
|
||||
x_off = (
|
||||
self.alignment_values[1][0] - self.alignment_values[k][0]
|
||||
) * 1000
|
||||
y_off = (
|
||||
self.alignment_values[k][1] - self.alignment_values[1][1]
|
||||
) * 1000
|
||||
f.write(f"{angle_deg}\t{x_off:.4f}\t{y_off:.4f}\n")
|
||||
print(
|
||||
f" Angle {angle_deg:3d} deg: "
|
||||
f"x_offset={x_off:.2f} um, y_offset={y_off:.2f} um"
|
||||
)
|
||||
|
||||
angles = np.array(LAMNI_ALIGNMENT_ANGLES, dtype=float)
|
||||
x_offsets = np.array(
|
||||
[
|
||||
(self.alignment_values[1][0] - self.alignment_values[k][0]) * 1000
|
||||
for k in range(1, 9)
|
||||
]
|
||||
)
|
||||
y_offsets = np.array(
|
||||
[
|
||||
(self.alignment_values[k][1] - self.alignment_values[1][1]) * 1000
|
||||
for k in range(1, 9)
|
||||
]
|
||||
)
|
||||
data = np.array([angles, x_offsets, y_offsets])
|
||||
|
||||
# Push to XRayEye widget: feeds waveform_x (row 1) and waveform_y (row 2)
|
||||
self.gui.submit_fit_array(data)
|
||||
print(f"Fit data submitted with shape {data.shape}:\n{data}")
|
||||
Binary file not shown.
@@ -41,10 +41,6 @@ def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
|
||||
class FlomniToolsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class FlomniInitError(Exception):
|
||||
pass
|
||||
|
||||
@@ -53,34 +49,6 @@ class FlomniError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
# class FlomniTools:
|
||||
# def yesno(self, message: str, default="none", autoconfirm=0) -> bool:
|
||||
# if autoconfirm and default == "y":
|
||||
# self.printgreen(message + " Automatically confirming default: yes")
|
||||
# return True
|
||||
# elif autoconfirm and default == "n":
|
||||
# self.printgreen(message + " Automatically confirming default: no")
|
||||
# return False
|
||||
# if default == "y":
|
||||
# message_ending = " [Y]/n? "
|
||||
# elif default == "n":
|
||||
# message_ending = " y/[N]? "
|
||||
# else:
|
||||
# message_ending = " y/n? "
|
||||
# while True:
|
||||
# user_input = input(self.OKBLUE + message + message_ending + self.ENDC)
|
||||
# if (
|
||||
# user_input == "Y" or user_input == "y" or user_input == "yes" or user_input == "Yes"
|
||||
# ) or (default == "y" and user_input == ""):
|
||||
# return True
|
||||
# if (
|
||||
# user_input == "N" or user_input == "n" or user_input == "no" or user_input == "No"
|
||||
# ) or (default == "n" and user_input == ""):
|
||||
# return False
|
||||
# else:
|
||||
# print("Please expicitely confirm y or n.")
|
||||
|
||||
|
||||
class FlomniInitStagesMixin:
|
||||
|
||||
def flomni_init_stages(self):
|
||||
|
||||
@@ -5,233 +5,424 @@ import os
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen
|
||||
|
||||
logger = bec_logger.logger
|
||||
# import builtins to avoid linter errors
|
||||
|
||||
bec = builtins.__dict__.get("bec")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
scans = builtins.__dict__.get("scans")
|
||||
|
||||
|
||||
def umv(*args):
|
||||
return scans.umv(*args, relative=False)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_ipython_client.plugins.omny import OMNY
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.lamni import LamNI
|
||||
|
||||
|
||||
# Laminography alignment angles: full 360° in 45° steps
|
||||
LAMNI_ALIGNMENT_ANGLES = [k * 45 for k in range(8)] # [0, 45, 90, 135, 180, 225, 270, 315]
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
# pixel calibration, multiply to get mm
|
||||
PIXEL_CALIBRATION = 0.2 / 218 # .2 with binning
|
||||
"""BEC-GUI-based X-ray eye alignment for LamNI.
|
||||
|
||||
def __init__(self, client, omny: OMNY) -> None:
|
||||
Replaces the old EPICS/LabView interface. Collects sample centre
|
||||
coordinates (x and y) at 8 equally-spaced laminography angles over the
|
||||
full 360°, submits them to the XRayEye widget's fit tab, and then reads
|
||||
the resulting sinusoidal fit parameters back via
|
||||
``lamni.align.read_xray_eye_correction_from_gui()``.
|
||||
|
||||
The key difference from the FlOMNI alignment is that LamNI needs *both*
|
||||
x and y fits because the tilted rotation axis (61°) couples both
|
||||
directions.
|
||||
"""
|
||||
|
||||
# Pixel calibration: multiply pixel coordinate by this to get mm
|
||||
PIXEL_CALIBRATION = 0.2 / 218 # mm/pixel (0.2 mm FOV, 218 px)
|
||||
|
||||
def __init__(self, client, lamni: LamNI) -> None:
|
||||
self.client = client
|
||||
self.omny = omny
|
||||
self.lamni = lamni
|
||||
self.device_manager = client.device_manager
|
||||
self.scans = client.scans
|
||||
self.alignment_values = {}
|
||||
self.omny.reset_correction()
|
||||
self.omny.reset_tomo_alignment_fit()
|
||||
# alignment_values[k] = [x_mm, y_mm]
|
||||
# k=0 : FZP centre
|
||||
# k=1 : sample at 0° (reference; shift_xy computed from k=0 vs k=1)
|
||||
# k=2 : sample at 45°
|
||||
# ...
|
||||
# k=8 : sample at 315°
|
||||
self.alignment_values: dict[int, list[float]] = {}
|
||||
# Reset correction state via the existing alignment object on lamni
|
||||
self.lamni.align.reset_correction()
|
||||
self.lamni.align.reset_xray_eye_correction()
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0, 0]
|
||||
self._xray_fov_xy = [0, 0]
|
||||
|
||||
def save_frame(self):
|
||||
epics_put("XOMNYI-XEYE-SAVFRAME:0", 1)
|
||||
|
||||
def update_frame(self):
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
# start live
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 1)
|
||||
# wait for start live
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
time.sleep(0.5)
|
||||
print("waiting for live view to start...")
|
||||
fshopen()
|
||||
|
||||
epics_put("XOMNYI-XEYE-ACQDONE:0", 0)
|
||||
|
||||
while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0:
|
||||
print("waiting for new frame...")
|
||||
time.sleep(0.5)
|
||||
|
||||
time.sleep(0.5)
|
||||
# stop live view
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 0)
|
||||
time.sleep(1)
|
||||
# fshclose
|
||||
print("got new frame")
|
||||
|
||||
def tomo_rotate(self, val: float):
|
||||
# pylint: disable=undefined-variable
|
||||
umv(self.device_manager.devices.osamroy, val)
|
||||
|
||||
def get_tomo_angle(self):
|
||||
return self.device_manager.devices.osamroy.readback.get()
|
||||
|
||||
def update_fov(self, k: int):
|
||||
self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0])
|
||||
self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0])
|
||||
# ------------------------------------------------------------------
|
||||
# GUI shortcut
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def movement_buttons_enabled(self):
|
||||
return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")]
|
||||
def gui(self):
|
||||
"""Return the live XRayEye RPC handle stored by LamniGuiTools."""
|
||||
return self.lamni.xeyegui
|
||||
|
||||
@movement_buttons_enabled.setter
|
||||
def movement_buttons_enabled(self, enabled: bool):
|
||||
enabled = int(enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", enabled)
|
||||
epics_put("XOMNYI-XEYE-ENAMVY:0", enabled)
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _reset_init_values(self):
|
||||
self.shift_xy = [0.0, 0.0] # base shift to bring sample to beam centre [µm]
|
||||
self._xray_fov_xy = [0.0, 0.0]
|
||||
|
||||
def tomo_rotate(self, val: float):
|
||||
umv(self.device_manager.devices.lsamrot, val)
|
||||
|
||||
def get_tomo_angle(self) -> float:
|
||||
return self.device_manager.devices.lsamrot.readback.read()["lsamrot"]["value"]
|
||||
|
||||
def _disable_rt_feedback(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_disable()
|
||||
|
||||
def _enable_rt_feedback(self):
|
||||
self.device_manager.devices.rtx.controller.feedback_enable_with_reset()
|
||||
|
||||
def update_frame(self, keep_shutter_open: bool = False):
|
||||
"""Capture a fresh camera frame.
|
||||
|
||||
Args:
|
||||
keep_shutter_open: If True the shutter is left open and the GUI
|
||||
stays in live-view mode after the frame is taken. Useful when
|
||||
it is hard to locate the sample between rotations. The caller
|
||||
is responsible for closing the shutter afterwards.
|
||||
"""
|
||||
if not dev.cam_xeye.live_mode_enabled.get():
|
||||
dev.cam_xeye.live_mode_enabled.put(True)
|
||||
self.gui.on_live_view_enabled(True)
|
||||
dev.omnyfsh.fshopen()
|
||||
time.sleep(0.5)
|
||||
if not keep_shutter_open:
|
||||
self.gui.on_live_view_enabled(False)
|
||||
time.sleep(0.1)
|
||||
dev.omnyfsh.fshclose()
|
||||
print("Received new frame.")
|
||||
else:
|
||||
print("Received new frame. Shutter remains open and live view active.")
|
||||
|
||||
def update_fov(self, k: int):
|
||||
self._xray_fov_xy[0] = max(
|
||||
getattr(dev.omny_xray_gui, f"width_x_{k}").get(), self._xray_fov_xy[0]
|
||||
)
|
||||
self._xray_fov_xy[1] = max(
|
||||
getattr(dev.omny_xray_gui, f"width_y_{k}").get(), self._xray_fov_xy[1]
|
||||
)
|
||||
|
||||
def movement_buttons_enabled(self, enable_x: bool, enable_y: bool):
|
||||
self.gui.on_motors_enable(enable_x, enable_y)
|
||||
|
||||
def send_message(self, msg: str):
|
||||
epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg)
|
||||
print(f"Alignment GUI: {msg}")
|
||||
self.gui.user_message = msg
|
||||
|
||||
def align(self):
|
||||
# reset shift xy and fov params
|
||||
# ------------------------------------------------------------------
|
||||
# Main alignment procedure
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def align(self, keep_shutter_open: bool = False):
|
||||
"""Run the full LamNI X-ray eye alignment.
|
||||
|
||||
Args:
|
||||
keep_shutter_open: If True the shutter is left open between angle
|
||||
steps so the sample remains visible in live view. At the end
|
||||
of the procedure the user is asked whether to close the shutter.
|
||||
Matches the equivalent FlOMNI option.
|
||||
|
||||
Procedure
|
||||
---------
|
||||
Step 0 – FZP centre
|
||||
Put FZP in, capture frame, user clicks FZP centre.
|
||||
|
||||
Step 1 – Sample at 0°
|
||||
Put sample in, capture frame, user clicks sample centre.
|
||||
Compute base shift (shift_xy) and move to scan centre.
|
||||
|
||||
Steps 2–8 – Sample at 45°, 90°, …, 315°
|
||||
Rotate to each angle (via lamni_move_to_scan_center so the base
|
||||
shift is applied), capture frame, user clicks sample centre.
|
||||
|
||||
After all submits
|
||||
Compute x/y offsets, push to GUI fit tab, wait for DAP fit,
|
||||
then load fit parameters into the global variable store.
|
||||
"""
|
||||
self.lamni.lamnigui_show_xeyealign()
|
||||
self.send_message("Getting things ready. Please wait...")
|
||||
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
# Initialise EPICS GUI device state
|
||||
dev.omny_xray_gui.mvx.set(0)
|
||||
dev.omny_xray_gui.mvy.set(0)
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self._reset_init_values()
|
||||
|
||||
self.tomo_rotate(0)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", 0)
|
||||
# --- Step 0: FZP centre ------------------------------------------
|
||||
self._disable_rt_feedback()
|
||||
self.lamni.lfzp_in()
|
||||
self._enable_rt_feedback()
|
||||
|
||||
self.omny.oeye_xray_in()
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
self.omny.feedback_enable_with_reset()
|
||||
|
||||
# disable movement buttons
|
||||
self.movement_buttons_enabled = False
|
||||
|
||||
sample_name = dev.omny_samples.get_sample_name_in_samplestage()
|
||||
epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", sample_name)
|
||||
|
||||
# this makes sure we are in a defined state
|
||||
self.omny.feedback_disable()
|
||||
|
||||
epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION)
|
||||
|
||||
osamx_in = self.omny.OMNYTools._get_user_param_safe("osamx", "in")
|
||||
umv(dev.osamx, osamx_in - 0.35)
|
||||
|
||||
self.omny.ofzp_in()
|
||||
self.update_frame()
|
||||
|
||||
# enable submit buttons
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-STEP:0", 0)
|
||||
self.send_message("Submit center value of FZP.")
|
||||
self.gui.enable_submit_button(True)
|
||||
dev.omny_xray_gui.step.set(0)
|
||||
self.send_message("Submit centre of FZP.")
|
||||
|
||||
k = 0
|
||||
while True:
|
||||
if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1:
|
||||
val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") / 2 * self.PIXEL_CALIBRATION # in mm
|
||||
self.alignment_values[k] = val_x
|
||||
print(f"Clicked position {k}: x {self.alignment_values[k]}")
|
||||
rtx_position = dev.rtx.readback.get() / 1000
|
||||
print(f"Current rtx position {rtx_position}")
|
||||
self.alignment_values[k] -= rtx_position
|
||||
print(f"Corrected position {k}: x {self.alignment_values[k]}")
|
||||
if dev.omny_xray_gui.submit.get() == 1:
|
||||
val_x = (
|
||||
getattr(dev.omny_xray_gui, f"xval_x_{k}").get()
|
||||
* self.PIXEL_CALIBRATION
|
||||
)
|
||||
val_y = (
|
||||
getattr(dev.omny_xray_gui, f"yval_y_{k}").get()
|
||||
* self.PIXEL_CALIBRATION
|
||||
)
|
||||
self.alignment_values[k] = [val_x, val_y]
|
||||
print(
|
||||
f"Clicked position {k}: "
|
||||
f"x={self.alignment_values[k][0]:.4f} mm, "
|
||||
f"y={self.alignment_values[k][1]:.4f} mm"
|
||||
)
|
||||
dev.omny_xray_gui.submit.set(0)
|
||||
|
||||
if k == 0: # received center value of FZP
|
||||
self.send_message("please wait ...")
|
||||
self.movement_buttons_enabled = False
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
# --- k=0: received FZP centre ----------------------------
|
||||
if k == 0:
|
||||
self.send_message("Please wait - moving sample in...")
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
self.omny.feedback_disable()
|
||||
osamx_in = self.omny.OMNYTools._get_user_param_safe("osamx", "in")
|
||||
umv(dev.osamx, osamx_in)
|
||||
self.lamni.loptics_out()
|
||||
self._disable_rt_feedback()
|
||||
time.sleep(0.3)
|
||||
self._enable_rt_feedback()
|
||||
|
||||
self.omny.ofzp_out()
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message("Find the sample and submit its centre.")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, True)
|
||||
|
||||
self.update_frame()
|
||||
epics_put("XOMNYI-XEYE-RECBG:0", 1)
|
||||
while epics_get("XOMNYI-XEYE-RECBG:0") == 1:
|
||||
time.sleep(0.5)
|
||||
print("waiting for background frame...")
|
||||
# --- k=1: sample at 0° - compute base shift --------------
|
||||
elif k == 1:
|
||||
# Displacement from FZP centre to sample centre gives the
|
||||
# correction needed to bring the sample onto the beam axis.
|
||||
# Sign conventions match the old alignment.py.
|
||||
self.shift_xy[0] += (
|
||||
self.alignment_values[0][0] - self.alignment_values[1][0]
|
||||
) * 1000 # um
|
||||
self.shift_xy[1] += (
|
||||
self.alignment_values[1][1] - self.alignment_values[0][1]
|
||||
) * 1000 # um
|
||||
print(
|
||||
f"Base shift: "
|
||||
f"x={self.shift_xy[0]:.2f} um, "
|
||||
f"y={self.shift_xy[1]:.2f} um"
|
||||
)
|
||||
|
||||
umv(dev.osamx, osamx_in)
|
||||
time.sleep(0.5)
|
||||
self.omny.feedback_enable_with_reset()
|
||||
self.send_message("Please wait - moving to scan centre...")
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.gui.enable_submit_button(False)
|
||||
|
||||
self.update_frame()
|
||||
self.send_message("Adjust sample height and submit center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
self.movement_buttons_enabled = True
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
time.sleep(1)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
|
||||
elif 1 <= k < 5: # received sample center value at samroy 0 ... 315
|
||||
self.send_message("please wait ...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1)
|
||||
self.movement_buttons_enabled = False
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
self.tomo_rotate(k * 45)
|
||||
epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle())
|
||||
self.update_frame()
|
||||
self.send_message("Submit sample center")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", 0)
|
||||
epics_put("XOMNYI-XEYE-ENAMVX:0", 1)
|
||||
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message("Submit sample centre and FOV (0 deg).")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, False)
|
||||
self.update_fov(k)
|
||||
|
||||
elif k == 5: # received sample center value at samroy 270 and done
|
||||
self.send_message("done...")
|
||||
epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button
|
||||
self.movement_buttons_enabled = False
|
||||
# --- k=2..8: sample at 45, 90, ..., 315 deg -------------
|
||||
elif 1 < k <= 8:
|
||||
self.send_message("Please wait - rotating...")
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
|
||||
target_angle = LAMNI_ALIGNMENT_ANGLES[k - 1] # 45...315 deg
|
||||
# Approach from halfway between previous and current angle
|
||||
# to reduce mechanical hysteresis (same as old alignment.py)
|
||||
prev_angle = LAMNI_ALIGNMENT_ANGLES[k - 2]
|
||||
approach_angle = prev_angle + (target_angle - prev_angle) / 2
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(approach_angle)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(target_angle)
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
|
||||
dev.omny_xray_gui.angle.set(self.get_tomo_angle())
|
||||
self.update_frame(keep_shutter_open)
|
||||
self.send_message(f"Submit sample centre ({target_angle} deg).")
|
||||
self.gui.enable_submit_button(True)
|
||||
self.movement_buttons_enabled(True, False)
|
||||
self.update_fov(k)
|
||||
|
||||
# --- k=9: all angles collected - finish ------------------
|
||||
elif k == 9:
|
||||
self.send_message("All angles collected - computing fit...")
|
||||
self.gui.enable_submit_button(False)
|
||||
self.movement_buttons_enabled(False, False)
|
||||
self.update_fov(k)
|
||||
break
|
||||
|
||||
k += 1
|
||||
epics_put("XOMNYI-XEYE-STEP:0", k)
|
||||
|
||||
_xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0")
|
||||
if _xrayeyalignmvx != 0:
|
||||
umvr(dev.rtx, _xrayeyalignmvx)
|
||||
print(f"Current rtx position {dev.rtx.readback.get() / 1000}")
|
||||
epics_put("XOMNYI-XEYE-MVX:0", 0)
|
||||
if k > 0:
|
||||
epics_put(f"XOMNYI-XEYE-STAGEPOSX:{k}", dev.rtx.readback.get() / 1000)
|
||||
time.sleep(3)
|
||||
self.update_frame()
|
||||
dev.omny_xray_gui.step.set(k)
|
||||
|
||||
# Handle live motor movement buttons.
|
||||
# Only active during k=0 (FZP) and k=1 (sample at 0 deg) – both
|
||||
# x and y movements accumulate into shift_xy and are applied via
|
||||
# lamni_move_to_scan_center so the laminography coordinate
|
||||
# transformation is correctly applied. This matches the old
|
||||
# alignment.py exactly.
|
||||
if k < 2:
|
||||
# allow movements, store movements to calculate center
|
||||
_xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0")
|
||||
if _xrayeyalignmvy != 0:
|
||||
self.omny.feedback_disable()
|
||||
umvr(dev.osamy, _xrayeyalignmvy / 1000)
|
||||
time.sleep(2)
|
||||
epics_put("XOMNYI-XEYE-MVY:0", 0)
|
||||
self.omny.feedback_enable_with_reset()
|
||||
self.update_frame()
|
||||
_mvx = dev.omny_xray_gui.mvx.get()
|
||||
_mvy = dev.omny_xray_gui.mvy.get()
|
||||
if _mvx != 0 or _mvy != 0:
|
||||
self.shift_xy[0] += _mvx
|
||||
self.shift_xy[1] += _mvy
|
||||
self.scans.lamni_move_to_scan_center(
|
||||
self.shift_xy[0] / 1000,
|
||||
self.shift_xy[1] / 1000,
|
||||
self.get_tomo_angle(),
|
||||
).wait()
|
||||
print(
|
||||
f"Current centre: "
|
||||
f"horizontal={self.shift_xy[0]:.2f} um, "
|
||||
f"vertical={self.shift_xy[1]:.2f} um"
|
||||
)
|
||||
dev.omny_xray_gui.mvx.set(0)
|
||||
dev.omny_xray_gui.mvy.set(0)
|
||||
self.update_frame(keep_shutter_open)
|
||||
|
||||
time.sleep(0.2)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# All submits received - compute offsets and push to GUI fit tab
|
||||
# ------------------------------------------------------------------
|
||||
self.write_output()
|
||||
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
|
||||
self.tomo_rotate(0)
|
||||
|
||||
umv(dev.rtx, 0)
|
||||
|
||||
# free camera
|
||||
epics_put("XOMNYI-XEYE-ACQ:0", 2)
|
||||
|
||||
print(
|
||||
f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy"
|
||||
f" = {fovy:.0f} microns"
|
||||
f"Largest FOV from X-ray eye alignment:\n"
|
||||
f" fovx = {fovx:.0f} um, fovy = {fovy:.0f} um"
|
||||
)
|
||||
print("Use the matlab routine to FIT the current alignment...")
|
||||
print(
|
||||
f"Base shift (applied to all scan centres):\n"
|
||||
f" x = {self.shift_xy[0]:.2f} um, y = {self.shift_xy[1]:.2f} um"
|
||||
)
|
||||
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
|
||||
|
||||
print("Then LOAD ALIGNMENT PARAMETERS by running omny.read_alignment_offset()\n")
|
||||
# Switch GUI to fit tab and wait for DAP to finish fitting
|
||||
self.lamni.lamnigui_show_xeyealign_fittab()
|
||||
print("Waiting 5 s for DAP sinusoidal fit to converge...")
|
||||
time.sleep(5)
|
||||
|
||||
# Read fit parameters from the GUI into the global variable store
|
||||
print("Loading new alignment parameters from X-ray eye GUI fit.")
|
||||
self.lamni.align.read_xray_eye_correction_from_gui()
|
||||
|
||||
if keep_shutter_open:
|
||||
answer = input("Close the shutter now? [Y/n]: ").strip().lower()
|
||||
if answer in ("", "y", "yes"):
|
||||
dev.omnyfsh.fshclose()
|
||||
self.gui.on_live_view_enabled(False)
|
||||
print("Shutter closed.")
|
||||
else:
|
||||
print("Shutter left open.")
|
||||
|
||||
# Return to 0 deg and reset scan centre
|
||||
self._disable_rt_feedback()
|
||||
self.tomo_rotate(0)
|
||||
umv(dev.rtx, 0)
|
||||
print(
|
||||
"Done. You are ready to remove the X-ray eye and start ptychography scans.\n"
|
||||
"Fine alignment: lamni.tomo_parameters(), then lamni.tomo_alignment_scan()"
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fit data preparation and submission
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def write_output(self):
|
||||
"""Compute x/y offsets for each angle and push them to the GUI fit tab.
|
||||
|
||||
Offsets are the displacement of the sample from its position at 0 deg
|
||||
(k=1 is the reference):
|
||||
x_offset[i] = (x_at_0deg - x_at_angle_i) * 1000 [um]
|
||||
y_offset[i] = (y_at_angle_i - y_at_0deg) * 1000 [um]
|
||||
|
||||
The array passed to submit_fit_array has shape (3, 8):
|
||||
row 0: angles [0, 45, ..., 315]
|
||||
row 1: x offsets [um]
|
||||
row 2: y offsets [um]
|
||||
"""
|
||||
# Archival text file (backward compatible with any external scripts)
|
||||
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
|
||||
if not os.path.exists(file):
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, "w") as alignment_values_file:
|
||||
alignment_values_file.write("angle\thorizontal\n")
|
||||
for k in range(1, 6):
|
||||
fovx_offset = self.alignment_values[0] - self.alignment_values[k]
|
||||
print(f"Writing to file new alignment: number {k}, value x {fovx_offset}")
|
||||
alignment_values_file.write(f"{(k-1)*45}\t{fovx_offset*1000}\n")
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, "w") as f:
|
||||
f.write("angle\thorizontal\tvertical\n")
|
||||
for k in range(1, 9):
|
||||
angle_deg = LAMNI_ALIGNMENT_ANGLES[k - 1]
|
||||
x_off = (
|
||||
self.alignment_values[1][0] - self.alignment_values[k][0]
|
||||
) * 1000
|
||||
y_off = (
|
||||
self.alignment_values[k][1] - self.alignment_values[1][1]
|
||||
) * 1000
|
||||
f.write(f"{angle_deg}\t{x_off:.4f}\t{y_off:.4f}\n")
|
||||
print(
|
||||
f" Angle {angle_deg:3d} deg: "
|
||||
f"x_offset={x_off:.2f} um, y_offset={y_off:.2f} um"
|
||||
)
|
||||
|
||||
angles = np.array(LAMNI_ALIGNMENT_ANGLES, dtype=float)
|
||||
x_offsets = np.array(
|
||||
[
|
||||
(self.alignment_values[1][0] - self.alignment_values[k][0]) * 1000
|
||||
for k in range(1, 9)
|
||||
]
|
||||
)
|
||||
y_offsets = np.array(
|
||||
[
|
||||
(self.alignment_values[k][1] - self.alignment_values[1][1]) * 1000
|
||||
for k in range(1, 9)
|
||||
]
|
||||
)
|
||||
data = np.array([angles, x_offsets, y_offsets])
|
||||
|
||||
# Push to XRayEye widget: feeds waveform_x (row 1) and waveform_y (row 2)
|
||||
self.gui.submit_fit_array(data)
|
||||
print(f"Fit data submitted with shape {data.shape}:\n{data}")
|
||||
@@ -297,4 +297,37 @@ cam_xeye:
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
readoutPriority: async
|
||||
|
||||
# ############################################################
|
||||
# ########## OMNY / flOMNI / LamNI fast shutter ##############
|
||||
# ############################################################
|
||||
omnyfsh:
|
||||
description: omnyfsh connects to fast shutter at X12 if device fsh exists
|
||||
deviceClass: csaxs_bec.devices.omny.shutter.OMNYFastShutter
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
############################################################
|
||||
#################### GUI Signals ###########################
|
||||
############################################################
|
||||
omny_xray_gui:
|
||||
description: Gui signals
|
||||
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayAlignGUI
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
|
||||
# calculated_signal:
|
||||
# description: Calculated signal from alignment for fit
|
||||
# deviceClass: ophyd_devices.ComputedSignal
|
||||
# deviceConfig:
|
||||
# compute_method: "def just_rand():\n return 42"
|
||||
# enabled: true
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
@@ -13,7 +13,8 @@ class OMNYXRayAlignGUI(Device):
|
||||
sample_name = Cpt(Signal, value=0)
|
||||
angle = Cpt(Signal, value=0)
|
||||
pixel_size = Cpt(Signal, value=0)
|
||||
submit = Cpt(EpicsSignal, name="submit", read_pv="XOMNYI-XEYE-SUBMIT:0", auto_monitor=True)
|
||||
submit = Cpt(Signal, value=0)
|
||||
# submit = Cpt(EpicsSignal, name="submit", read_pv="XOMNYI-XEYE-SUBMIT:0", auto_monitor=True)
|
||||
step = Cpt(Signal, value=0)
|
||||
recbg = Cpt(Signal, value=0)
|
||||
mvx = Cpt(Signal, value=0)
|
||||
|
||||
@@ -180,28 +180,32 @@ class LamNIMoveToScanCenter(RequestBase, LamNIMixin):
|
||||
scan_name = "lamni_move_to_scan_center"
|
||||
scan_report_hint = None
|
||||
scan_type = "step"
|
||||
required_kwargs = []
|
||||
arg_input = {
|
||||
"shift_x": ScanArgType.FLOAT,
|
||||
"shift_y": ScanArgType.FLOAT,
|
||||
"angle": ScanArgType.FLOAT,
|
||||
}
|
||||
arg_bundle_size = {"bundle": len(arg_input), "min": 1, "max": 1}
|
||||
required_kwargs = ["shift_x", "shift_y", "angle"]
|
||||
arg_input = {}
|
||||
arg_bundle_size = {"bundle": 0, "min": 0, "max": 0}
|
||||
|
||||
def __init__(self, *args, parameter=None, **kwargs):
|
||||
"""
|
||||
Move LamNI to a new scan center.
|
||||
|
||||
Args:
|
||||
*args: shift x, shift y, tomo angle in deg
|
||||
shift_x (float): shift x in mm
|
||||
shift_y (float): shift y in mm
|
||||
angle (float): tomo angle in degrees
|
||||
|
||||
Examples:
|
||||
>>> scans.lamni_move_to_scan_center(1.2, 2.8, 12.5)
|
||||
>>> scans.lamni_move_to_scan_center(shift_x=1.2, shift_y=2.8, angle=12.5)
|
||||
"""
|
||||
super().__init__(parameter=parameter, **kwargs)
|
||||
scan_kwargs = parameter.get("kwargs", {})
|
||||
self.shift_x = float(scan_kwargs.get("shift_x", 0))
|
||||
self.shift_y = float(scan_kwargs.get("shift_y", 0))
|
||||
self.angle = float(scan_kwargs.get("angle", 0))
|
||||
|
||||
def run(self):
|
||||
center_x, center_y = self._lamni_compute_scan_center(*self.caller_args)
|
||||
center_x, center_y = self._lamni_compute_scan_center(
|
||||
self.shift_x, self.shift_y, self.angle
|
||||
)
|
||||
yield from self.lamni_new_scan_center_interferometer(center_x, center_y)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
(user.ptychography.lamni)=
|
||||
|
||||
# LamNI
|
||||
|
||||
LamNI is an instrument for 3D ptychography via ptychographic X-ray computed lamninography (PyXL). The instrument is described in detail [here](https://www.dora.lib4ri.ch/psi/islandora/object/psi:33067).
|
||||
LamNI is an instrument for 3D ptychography via ptychographic X-ray computed laminography (PyXL). The instrument is described in detail [here](https://www.dora.lib4ri.ch/psi/islandora/object/psi:33067).
|
||||
|
||||
## How to LamNI
|
||||
## How to LamNI
|
||||
|
||||
… a step-by-step guide for _beamline staff and expert users_.
|
||||
… a step-by-step guide for *beamline staff and expert users*.
|
||||
|
||||
### Sample change and alignment
|
||||
|
||||
@@ -17,70 +18,89 @@ Mount the new sample. The X-ray eye is already in, but the X-ray optics needs to
|
||||
|
||||
#### Coarse axis alignment
|
||||
|
||||
The effective position of the axis of rotation shifts with sample thickness or mounting position of the sample along the axis of rotation. The position of the axis of rotation is controlled by user parameters __center__ of the __lsamx__ and __lsamy__ stages. To observe the axis of rotation obtain the position of the Fresnel zone plate on the X-ray eye, possibly in the _ueye gui_ by:
|
||||
The effective position of the axis of rotation shifts with sample thickness or mounting position of the sample along the axis of rotation. The position of the axis of rotation is controlled by user parameters **center** of the **lsamx** and **lsamy** stages. To observe the axis of rotation obtain the position of the Fresnel zone plate on the X-ray eye, possibly in the *ueye gui* by:
|
||||
|
||||
1. `lamni.lfzp_in()`, move the FZP in
|
||||
1. `dev.rtx.controller.feedback_disable()`, disable feedback to allow lsam movements
|
||||
1. `fshopen()`, open the shutter
|
||||
1. `umv(dev.lsamrot,90)` to rotate the sample. One might observe the center of rotation at 0 and 180 degress.
|
||||
1. `umvr(dev.lsamx,0.01)` to move lsamx and lsamy such that the center of rotation is at the center of the X-ray beam
|
||||
1. `dev.lsamx` and `dev.lsamy` will print current position and the center value. Update the center value by
|
||||
`dev.lsamx.update_user_parameter({'center':8.69})`
|
||||
`dev.lsamy.update_user_parameter({'center':8.69})`
|
||||
1. close the shutter: `dev.omnyfsh.fshclose()`
|
||||
2. `dev.rtx.controller.feedback_disable()`, disable feedback to allow lsam movements
|
||||
3. `fshopen()`, open the shutter
|
||||
4. `umv(dev.lsamrot,90)` to rotate the sample. One might observe the center of rotation at 0 and 180 degrees.
|
||||
5. `umvr(dev.lsamx,0.01)` to move lsamx and lsamy such that the center of rotation is at the center of the X-ray beam
|
||||
6. `dev.lsamx` and `dev.lsamy` will print current position and the center value. Update the center value by
|
||||
`dev.lsamx.update_user_parameter({'center':8.69})`
|
||||
`dev.lsamy.update_user_parameter({'center':8.69})`
|
||||
7. close the shutter: `dev.omnyfsh.fshclose()`
|
||||
|
||||
#### X-ray eye alignment
|
||||
|
||||
The GUI on the windows computer is used to obtain a coarse sample alignment. Start the alignment process (and clear any previous alignment) by
|
||||
`lamni.align.align()`. With LamNI it can be very difficult to follow a region of interest as the sample rotates. Therefore the X-ray shutter will be open during the entire process. Therefore the windows software has to be set on __FORCE__ to continuously update frames and not freeze frames after rotation.
|
||||
- run `SPEC_ptycho_align.m` (in matlab, use __force_ptychography = 0__)
|
||||
- `lamni.align.read_xray_eye_correction()` to read the alignment parameters. The correction is based on sinusoidal fits in x and y direction. The values are computed by
|
||||
`lamni_compute_additional_correction_xeye_mu(angle)`
|
||||
- If slits were opened during alignment, close the slits `slits 1 to around 0.3`
|
||||
- `lamni.leye_out()` remove the X-ray eye and move the flight tube in
|
||||
- _possibly check slit0wh, idgap_
|
||||
The BEC GUI is used to obtain a coarse sample alignment. Start the alignment process (which clears any previous alignment) by
|
||||
`lamni.xrayeye_alignment_start()`
|
||||
|
||||
To only see one frame on the Windows GUI run `lamni.align.update_frame()`
|
||||
This opens the X-ray eye widget automatically. The procedure collects the sample centre position at 8 angles (0°–315° in 45° steps, full 360° rotation). At each angle the user clicks the sample centre in the image and presses **Submit**. After all 8 angles the data is sent to the **Fit** tab of the GUI where a sinusoidal fit runs automatically in both x and y directions. The fit parameters are loaded automatically at the end of the procedure.
|
||||
|
||||
With LamNI it can be difficult to relocate the sample between rotations. To keep the shutter open throughout, pass:
|
||||
`lamni.xrayeye_alignment_start(keep_shutter_open=True)`
|
||||
|
||||
To manually reload the fit parameters after the procedure has completed:
|
||||
`lamni.read_xray_eye_correction_from_gui()`
|
||||
**Note:** this reads from the live GUI widget via the `omny_xray_gui` device. It only works as long as the XRayEye GUI window remains open. If the window has been closed, reload from the archived text files instead:
|
||||
`lamni.read_xray_eye_correction()`
|
||||
(these files are written to `~/Data10/specES1/internal/xrayeye_alignmentvalues` at the end of every alignment run)
|
||||
|
||||
The correction is applied at each projection angle via
|
||||
`lamni.lamni_compute_additional_correction_xeye_mu(angle)`
|
||||
which is called automatically inside `lamni.tomo_scan_projection()`.
|
||||
|
||||
To capture a single fresh frame without running the full alignment:
|
||||
`lamni.xrayeye_update_frame()`
|
||||
or with the shutter left open: `lamni.xrayeye_update_frame(keep_shutter_open=True)`
|
||||
|
||||
* If slits were opened during alignment, close the slits: `slits 1` to around 0.3
|
||||
* `lamni.leye_out()` remove the X-ray eye and move the flight tube in
|
||||
* *possibly check slit0wh, idgap*
|
||||
|
||||
#### Fine alignment
|
||||
|
||||
The sample fine alignment can be obtained using ptychography. For this a short laminogram has to be recorded.
|
||||
- `lamni.tomo_parameters()` adjust the parameters for a coarse scan: A large step size and large FOV. Especially select __FOV offset = 0__ and __number of projections = 96__ (only one sub-laminogram will be recorded).
|
||||
- `lamni.sub_tomo_scan(1,0)` record one sub-laminogram
|
||||
- use the corresponding scan numbers in `SPEC_ptycho_align.m`
|
||||
- Record a last projection for all scans to reconstruct `lamni.tomo_scan_projection(0)` and wait for the reconstructions to be complete
|
||||
- Run `SPEC_ptycho_align.m` (in Matlab, __force ptycho=1__, and __correct scan numbers__)
|
||||
- Click the sample position in the Matlab GUI and then load the generated file by, for example
|
||||
`lamni.align.read_additional_correction('/sls/X12SA/data/e20632/Data10/cxs_software/ptycho/correction_lamni_um_S05389_lamni_fit.txt')`
|
||||
- With this alignment a second iteration could be performed. To read the second correction file use `lamni.align.read_additional_correction_2()`
|
||||
|
||||
* `lamni.tomo_parameters()` adjust the parameters for a coarse scan: A large step size and large FOV. Especially select **FOV offset = 0** and **number of projections = 96** (only one sub-laminogram will be recorded).
|
||||
* `lamni.sub_tomo_scan(1,0)` record one sub-laminogram
|
||||
* use the corresponding scan numbers in `SPEC_ptycho_align.m`
|
||||
* Record a last projection for all scans to reconstruct `lamni.tomo_scan_projection(0)` and wait for the reconstructions to be complete
|
||||
* Run `SPEC_ptycho_align.m` (in Matlab, **force ptycho=1**, and **correct scan numbers**)
|
||||
* Click the sample position in the Matlab GUI and then load the generated file by, for example
|
||||
`lamni.read_additional_correction('/sls/X12SA/data/e20632/Data10/cxs_software/ptycho/correction_lamni_um_S05389_lamni_fit.txt')`
|
||||
* With this alignment a second iteration could be performed. To read the second correction file use `lamni.read_additional_correction_2()`
|
||||
|
||||
#### Shifting the FOV
|
||||
|
||||
- `lamni.align.tomo_fovx/y_offset=value` [mm] will shift the field of view. Perform this adjustment from projections collected at __lsamrot 0 degrees__.
|
||||
* `lamni.tomo_fovx/y_offset=value` [mm] will shift the field of view. Perform this adjustment from projections collected at **lsamrot 0 degrees**. This shift will rotate. In contrast the manual shift will be a constant shift, identical at all angles.
|
||||
|
||||
### Laminography scan
|
||||
|
||||
Start the laminography scan by
|
||||
|
||||
1. `lamni.tomo_parameters()` adjust the parameters to the desired settings.
|
||||
1. for test scans run
|
||||
`lamni.tomo_scan_projection(angle)`
|
||||
`lamni.tomo_reconstruct()`
|
||||
1. `lamni.tomo_scan()` to start the lamninography scan
|
||||
2. for test scans run
|
||||
`lamni.tomo_scan_projection(angle)`
|
||||
`lamni.tomo_reconstruct()`
|
||||
3. `lamni.tomo_scan()` to start the laminography scan
|
||||
|
||||
### Tips and Tricks
|
||||
|
||||
#### Reset corrections
|
||||
|
||||
- `lamni.align.reset_correction()`
|
||||
- `lamni.align.reset_correction_2()`
|
||||
- `lamni.align.reset_xray_eye_correction()`
|
||||
* `lamni.reset_correction()`
|
||||
* `lamni.reset_correction_2()`
|
||||
* `lamni.reset_xray_eye_correction()`
|
||||
|
||||
#### Adjusting beam size with feedback running
|
||||
|
||||
If the beam size needs to be changed with feedback running, e.g. to switch from near-field to far-field ptychography, following steps can be taken:
|
||||
If the beam size needs to be changed with feedback running, e.g. to switch from near-field to far-field ptychography, following steps can be taken:
|
||||
|
||||
1. `dev.loptz.enable_set=True` to enable loptz movements with feedback running
|
||||
1. `umvr(dev.lopz,_value_)` move loptz to the desired position.
|
||||
1. `lamni._manual_shift_x/y = _value_` correct the stage run out from motion along the optical axis (units of um). The exact value can be checked by comparing feature positions in projections before/after adjusting loptz.
|
||||
1. Potentially correct the alignment of the OSA
|
||||
2. `umvr(dev.lopz,_value_)` move loptz to the desired position.
|
||||
3. `lamni._manual_shift_x/y = _value_` correct the stage run out from motion along the optical axis (units of um). The exact value can be checked by comparing feature positions in projections before/after adjusting loptz.
|
||||
4. Potentially correct the alignment of the OSA
|
||||
|
||||
#### BEC tips
|
||||
|
||||
@@ -97,8 +117,8 @@ This part of the manual describes the software structure in more detail.
|
||||
The nano-positioning is controlled by a feedback loop running on a real-time linux based computer. With all related hardware connected, this loop has to be started manually.
|
||||
|
||||
1. Login to the computer by `ssh control@mpc2680`. The password is written on the physical machine.
|
||||
1. `cd OMNY/lamni/`
|
||||
1. `./startLAMNI`
|
||||
2. `cd OMNY/lamni/`
|
||||
3. `./startLAMNI`
|
||||
|
||||
Once the loop has started, it is possible to start bec with the LamNI configuration file.
|
||||
|
||||
@@ -123,60 +143,64 @@ The stages of LamNI are referenced in respect to their endswitches or reference
|
||||
Show the status of all galil controllers (all stepper motors and the UPR rotation stage)
|
||||
`dev.lsamx.controller.galil_show_all()`
|
||||
|
||||
The same holds true for the Smaract stages which control the OSA position. Their status can be checked by
|
||||
The same holds true for the Smaract stages which control the OSA position. Their status can be checked by
|
||||
`dev.losax.controller.show_all()`
|
||||
|
||||
In case referencing of the LamNI stages is required, run
|
||||
`lamni.init.lamni_init_stages()`
|
||||
This script will first verify that the stages are not in an initialized state, and then reference all stages in a safe way. The user will be warned in case of a potentially risky situation. This mainly involves a collision risk upstream with the exposure box exit window. It might be worth to check clearance prior to calling the init skript.
|
||||
This script will first verify that the stages are not in an initialized state, and then reference all stages in a safe way. The user will be warned in case of a potentially risky situation. This mainly involves a collision risk upstream with the exposure box exit window. It might be worth to check clearance prior to calling the init script.
|
||||
|
||||
### Interferometer
|
||||
|
||||
The position feedback in LamNI is controlled in closed loop to an interferometric position measurement. To show the signal of the interferometers:
|
||||
`lamni.show_signal_strength_interferometer()`
|
||||
Typical values with proper alignment are
|
||||
_TODO_
|
||||
*TODO*
|
||||
|
||||
#### Interferometer feedback commands
|
||||
|
||||
- `dev.rtx.feedback_enable_with_reset()`
|
||||
- `dev.rtx.feedback_disable()`
|
||||
- `dev.rtx.feedback_enable_without_reset()` *is only used internally by lamni methods
|
||||
- if reset of angle interferometer is required
|
||||
`dev.rtx.feedback_disable_and_even_reset_lamni_angle_interferometer()`
|
||||
- `dev.rtx.feedback_enable_with_reset()`
|
||||
* `dev.rtx.feedback_enable_with_reset()`
|
||||
* `dev.rtx.feedback_disable()`
|
||||
* `dev.rtx.feedback_enable_without_reset()` \*is only used internally by lamni methods
|
||||
* if reset of angle interferometer is required
|
||||
`dev.rtx.feedback_disable_and_even_reset_lamni_angle_interferometer()`
|
||||
* `dev.rtx.feedback_enable_with_reset()`
|
||||
|
||||
_ToDo Feedback status might be helpful. Plus make accessible via lamni.methods…_
|
||||
*ToDo Feedback status might be helpful. Plus make accessible via lamni.methods…*
|
||||
|
||||
### Scanning in 2D and sample alignment
|
||||
|
||||
The underlying scan function can be called as
|
||||
`scans.lamni_fermat_scan()`
|
||||
|
||||
Use `scans.lamni_fermat_scan?`for detailed information. A prerequisite for scanning is a running feedback system.
|
||||
Use `scans.lamni_fermat_scan?` for detailed information. A prerequisite for scanning is a running feedback system.
|
||||
|
||||
### GUI tools
|
||||
|
||||
During operation the BEC GUI will show the relevant cameras or progress information. To manually switch view TAB completion on 'lamni.lamnigui_' will show all options to control the GUI. Most useful
|
||||
'lamni.lamnigui_show_progress()' will show the measurement progress GUI
|
||||
'lamnigui_show_xeyealign()' will show the XrayEye alignment GUI
|
||||
During operation the BEC GUI will show the relevant cameras or progress information. TAB completion on `lamni.lamnigui_` will show all options to control the GUI. Most useful:
|
||||
|
||||
* `lamni.lamnigui_show_progress()` — shows the measurement progress with ring progress bar
|
||||
* `lamni.lamnigui_show_xeyealign()` — opens the X-ray eye alignment GUI on the Alignment tab
|
||||
* `lamni.lamnigui_show_xeyealign_fittab()` — opens the X-ray eye GUI on the Fit tab to inspect the sinusoidal fit
|
||||
* `lamni.lamnigui_idle()` — shows the LamNI idle splash screen
|
||||
* `lamni.lamnigui_docs()` — opens the PDF documentation viewer
|
||||
|
||||
### X-ray optics alignment
|
||||
|
||||
The positions of the optics stages are stored as stage parameters and are thus linked to the configuration file.
|
||||
Example: The OSAx “in” position can be reviewed by `dev.losax.user_parameter`
|
||||
Example: The OSAx "in" position can be reviewed by `dev.losax.user_parameter`
|
||||
Update the value by (example "losax", "in") by `dev.losax.update_user_parameter({"in":value})`
|
||||
|
||||
`lamni.lfzp_info()` shows info about the available FZPs at the current energy of the beamline. Optional parameter is the photon _energy_ in keV.
|
||||
`lamni.lfzp_info()` shows info about the available FZPs at the current energy of the beamline. Optional parameter is the photon *energy* in keV.
|
||||
Example: `lamni.lfzp_info(6.2)`
|
||||
|
||||
The laser feedback will be disabled and fine alignment lost if foptx/y are moved!
|
||||
|
||||
Following functions exist to move the optics in and out, the naming is self-explaining.
|
||||
- `lamni.lfzp_in()`
|
||||
- `lamni.loptics_in()`
|
||||
- `lamni.loptics_out()`
|
||||
- `lamni.losa_in()`
|
||||
- `lamni.losa_out()`
|
||||
- `lamni.lfzp_in()`
|
||||
|
||||
* `lamni.lfzp_in()`
|
||||
* `lamni.loptics_in()`
|
||||
* `lamni.loptics_out()`
|
||||
* `lamni.losa_in()`
|
||||
* `lamni.losa_out()`
|
||||
* `lamni.lfzp_in()`
|
||||
|
||||
@@ -214,7 +214,7 @@ This part of the manual describes the software structure in more detail.
|
||||
|
||||
The nano-positioning is controlled by a feedback loop running on a real-time linux based computer. With all related hardware connected, this loop has to be started manually.
|
||||
|
||||
1. Login to the computer by `ssh control@mpc3217`. The password is "engine".
|
||||
1. Login to the computer by `ssh control@mpc3217`. The password is written on the physical machine.
|
||||
1. `cd OMNY/OMNY/`
|
||||
1. `./startOMNY`
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ from unittest import mock
|
||||
|
||||
from bec_lib.device import DeviceBase
|
||||
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI import LamNI, XrayEyeAlign
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI import LamNI
|
||||
from csaxs_bec.bec_ipython_client.plugins.LamNI.x_ray_eye_align import XrayEyeAlign
|
||||
|
||||
# pylint: disable=unused-import
|
||||
|
||||
@@ -27,6 +28,7 @@ class RTMock(DeviceBase):
|
||||
|
||||
|
||||
def test_save_frame(bec_client_mock):
|
||||
# TODO - This test can be remove!
|
||||
client = bec_client_mock
|
||||
client.device_manager.devices.xeye = DeviceBase(
|
||||
name="xeye",
|
||||
@@ -42,6 +44,7 @@ def test_save_frame(bec_client_mock):
|
||||
|
||||
|
||||
def test_update_frame(bec_client_mock):
|
||||
# TODO - This test needs to be revisited, does no longer use the EPICS epics_put/get but device manager methods.
|
||||
epics_put = "csaxs_bec.bec_ipython_client.plugins.LamNI.alignment.epics_put"
|
||||
epics_get = "csaxs_bec.bec_ipython_client.plugins.LamNI.alignment.epics_get"
|
||||
fshopen = "csaxs_bec.bec_ipython_client.plugins.LamNI.alignment.fshopen"
|
||||
@@ -69,6 +72,7 @@ def test_update_frame(bec_client_mock):
|
||||
|
||||
|
||||
def test_disable_rt_feedback(bec_client_mock):
|
||||
# TODO - This test makes sense, check if it works correctly
|
||||
client = bec_client_mock
|
||||
client.device_manager.devices.xeye = DeviceBase(
|
||||
name="xeye",
|
||||
@@ -88,6 +92,7 @@ def test_disable_rt_feedback(bec_client_mock):
|
||||
|
||||
|
||||
def test_enable_rt_feedback(bec_client_mock):
|
||||
# TODO - This test makes sense, check if it works correctly
|
||||
client = bec_client_mock
|
||||
client.device_manager.devices.xeye = DeviceBase(
|
||||
name="xeye",
|
||||
@@ -107,6 +112,7 @@ def test_enable_rt_feedback(bec_client_mock):
|
||||
|
||||
|
||||
def test_tomo_rotate(bec_client_mock):
|
||||
# TODO - This test makes sense, check if it works correctly
|
||||
import builtins
|
||||
|
||||
client = bec_client_mock
|
||||
|
||||
Reference in New Issue
Block a user