Compare commits

...

20 Commits

Author SHA1 Message Date
x12sa
43ae732e34 added check for optics in etc to only move if not already there
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m59s
CI for csaxs_bec / test (push) Successful in 1m56s
2026-03-17 16:07:17 +01:00
x12sa
583b15b772 fix number projections when starting with start_angle, correction in progress reporting
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m58s
2026-03-17 15:29:23 +01:00
x12sa
4967474271 removed fsh commands
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m57s
2026-03-17 14:48:05 +01:00
x12sa
8d6a2b0f5c writing to spec log file removed (was for old webpage) 2026-03-17 14:46:09 +01:00
x12sa
dcde0e783e fix angle reversal at start with start angle pecified
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m59s
2026-03-17 14:42:52 +01:00
x12sa
58cd6bdaf7 fixes run 1
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m58s
2026-03-17 14:14:14 +01:00
x12sa
68320e1944 check tracker signal before rt move, which is otherwise stuck forever.
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m54s
2026-03-16 17:55:37 +01:00
x12sa
5ff32decc4 gui delay startup, flomni loading params fix 2026-03-16 17:54:56 +01:00
x12sa
2c31d79f1b loading of fit params from gui and from files
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m56s
2026-03-16 12:39:35 +01:00
x12sa
3ce6bbc134 check attr exist in gui remove all docs
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m55s
2026-03-16 12:10:29 +01:00
7c89086ba2 fix(ddg): Fix tests
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m56s
CI for csaxs_bec / test (push) Successful in 1m57s
2026-03-13 14:19:41 +01:00
1eb2961b7f fix(flomni): Fix logic for flomni scan, avoid resetting positions.
Some checks failed
CI for csaxs_bec / test (push) Failing after 1m52s
CI for csaxs_bec / test (pull_request) Failing after 1m54s
2026-03-13 14:09:35 +01:00
9d58dcfb83 fix(mcs): Fix timing on mcs card to resolve during complete. 2026-03-13 14:06:54 +01:00
541813a02e fix(ddg): Fix timing and delays on ddg. 2026-03-13 14:06:54 +01:00
4b04e7a35d fix: alarm handler signature for flomni scan
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 2m19s
CI for csaxs_bec / test (push) Successful in 1m55s
2026-03-13 09:34:32 +01:00
00c45b2bcf fix(gui): submit button do not block if fails
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m56s
CI for csaxs_bec / test (push) Successful in 1m59s
2026-03-12 23:42:20 +01:00
138b2668b3 fix(gui): cleanup adjusted 2026-03-12 23:42:20 +01:00
31eb00bd97 fix(gui): wrong api calls 2026-03-12 23:42:20 +01:00
x12sa
53e7593b8e fixed centering routine fsamx movements
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m56s
2026-03-12 22:31:56 +01:00
x12sa
8c7e1cf060 fixes during first round of testing flomni with Ana
Some checks failed
CI for csaxs_bec / test (pull_request) Successful in 1m55s
CI for csaxs_bec / test (push) Has been cancelled
2026-03-12 13:48:24 +01:00
19 changed files with 561 additions and 250 deletions

View File

@@ -356,18 +356,6 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
# Logging helpers
# ------------------------------------------------------------------
def write_to_spec_log(self, content):
try:
with open(
os.path.expanduser(
"~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log"
),
"a",
) as log_file:
log_file.write(content)
except Exception:
logger.warning("Failed to write to spec log file (omny web page).")
def write_to_scilog(self, content, tags: list = None):
try:
if tags is not None:
@@ -439,7 +427,6 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle},"
f" scan number {bec.queue.next_scan_number}.\n"
)
self.write_to_spec_log(log_message)
corridor_size = self.corridor_size if self.corridor_size > 0 else None
scans.lamni_fermat_scan(
fov_size=[self.lamni_piezo_range_x, self.lamni_piezo_range_y],
@@ -779,7 +766,7 @@ class LamNI(LamNIOpticsMixin, LamniGuiTools):
user_input = input("Are these parameters correctly set for your scan? ")
if user_input == "y":
print("good then")
print("OK. continue.")
return
self.tomo_countingtime = self._get_val("<ctime> s", self.tomo_countingtime, float)

View File

@@ -5,7 +5,7 @@ from rich import box
from rich.console import Console
from rich.table import Table
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools
dev = builtins.__dict__.get("dev")
@@ -172,7 +172,7 @@ class LamNIOpticsMixin:
def leye_out(self):
self.loptics_in()
fshclose()
dev.omnyfsh.fshopen()
leyey_out = self._get_user_param_safe("leyey", "out")
umv(dev.leyey, leyey_out)

View File

@@ -84,6 +84,15 @@ class FlomniInitStagesMixin:
else:
return
sensor_voltage_target = dev.ftransy.user_parameter.get("sensor_voltage")
sensor_voltage = float(dev.ftransy.controller.socket_put_and_receive("MG@AN[1]").strip())
if not np.isclose(sensor_voltage, sensor_voltage_target, 0.25):
print(f"Sensor voltage of the gripper is {sensor_voltage}, while target from config is {sensor_voltage_target}")
print("Verify that the value is acceptable and update config file. Reload config and start again.")
return
print("Starting to drive ftransy to +y limit")
self.drive_axis_to_limit(dev.ftransy, "forward")
dev.ftransy.limits = [-100, 0]
@@ -111,7 +120,7 @@ class FlomniInitStagesMixin:
if self.OMNYTools.yesno("Init of foptz. Can the stage move to the upstream limit without collision?"):
print("good then")
print("OK. continue.")
else:
return
@@ -165,7 +174,7 @@ class FlomniInitStagesMixin:
print("done")
if self.OMNYTools.yesno("Init of tracking stages. Did you remove the outer laser flight tubes?"):
print("good then")
print("OK. continue.")
else:
print("Stopping.")
return
@@ -181,7 +190,7 @@ class FlomniInitStagesMixin:
print("done")
if self.OMNYTools.yesno("Init of sample stage. Is the piezo at about 0 deg?"):
print("good then")
print("OK. continue.")
else:
print("Stopping.")
return
@@ -198,7 +207,7 @@ class FlomniInitStagesMixin:
print("Initializing UPR stage.")
if self.OMNYTools.yesno("To ensure that the end switches work, please check that they are currently not pushed. Is everything okay?"):
print("good then")
print("OK. continue.")
else:
print("Stopping.")
return
@@ -219,7 +228,7 @@ class FlomniInitStagesMixin:
continue
break
if self.OMNYTools.yesno("Shall I start the index search?"):
print("good then. Starting index search.")
print("OK. continue.. Starting index search.")
else:
print("Stopping.")
return
@@ -238,7 +247,7 @@ class FlomniInitStagesMixin:
print("done")
if self.OMNYTools.yesno("Init of foptx. Can the stage move to the positive limit without collision? Attention: tracker flight tube!"):
print("good then")
print("OK. continue.")
else:
print("Stopping.")
return
@@ -262,7 +271,7 @@ class FlomniInitStagesMixin:
break
if self.OMNYTools.yesno("Start limit switch search of fopty?"):
print("good then")
print("OK. continue.")
else:
print("Stopping.")
return
@@ -414,6 +423,7 @@ class FlomniSampleTransferMixin:
raise FlomniError("Ftray is not at the 'IN' position. Aborting.")
def ftransfer_flomni_stage_in(self):
time.sleep(1)
sample_in_position = dev.flomni_samples.is_sample_slot_used(0)
#bool(float(dev.flomni_samples.sample_placed.sample0.get()))
if not sample_in_position:
@@ -428,7 +438,19 @@ class FlomniSampleTransferMixin:
umv(dev.fsamx, fsamx_in)
dev.fsamx.limits = [fsamx_in - 0.4, fsamx_in + 0.4]
self.flomnigui_idle()
print("Moving X-ray eye in.")
if self.OMNYTools.yesno("Please confirm that this is ok with the flight tube. This check is to be removed after commissioning", "n"):
print("OK. continue.")
else:
print("Stopping.")
raise FlomniError("Manual abort of x-ray eye in.")
self.feye_in()
print("Moving X-ray optics out.")
self.foptics_out()
self.xrayeye_update_frame()
def laser_tracker_show_all(self):
dev.rtx.controller.laser_tracker_show_all()
@@ -533,12 +555,6 @@ class FlomniSampleTransferMixin:
self.flomnigui_show_cameras()
if self.OMNYTools.yesno("Please confirm that there is currently no sample in the gripper. It would be dropped!", "y"):
print("good then")
else:
print("Stopping.")
raise FlomniError("The sample transfer was manually aborted.")
self.ftransfer_gripper_move(position)
self.ftransfer_controller_enable_mount_mode()
@@ -586,6 +602,8 @@ class FlomniSampleTransferMixin:
if sample_in_position:
raise FlomniError(f"The planned put position [{position}] already has a sample.")
self.flomnigui_show_cameras()
self.ftransfer_gripper_move(position)
self.ftransfer_controller_enable_mount_mode()
@@ -611,7 +629,7 @@ class FlomniSampleTransferMixin:
self.ftransfer_controller_disable_mount_mode()
self.ensure_gripper_up()
sample_name = dev.flomni_samples.sample_in_gripper.get()
sample_name = dev.flomni_samples.sample_in_gripper_name.get()
self.flomni_modify_storage_non_interactive(100, 0, "-")
self.flomni_modify_storage_non_interactive(position, 1, sample_name)
@@ -739,7 +757,7 @@ class FlomniSampleTransferMixin:
return
if self.OMNYTools.yesno("All OK? Continue?", "y"):
print("good then")
print("OK. continue.")
dev.ftransy.controller.socket_put_confirmed("confirm=1")
else:
print("Stopping.")
@@ -773,7 +791,7 @@ class FlomniSampleTransferMixin:
if position == 0 and fsamx_pos > -160:
if self.OMNYTools.yesno("May the flomni stage be moved out for the sample change? Feedback will be disabled and alignment will be lost!", "y"):
print("good then")
print("OK. continue.")
self.ftransfer_flomni_stage_out()
else:
print("Stopping.")
@@ -971,56 +989,63 @@ class FlomniAlignmentMixin:
def read_alignment_offset(
self,
dir_path=os.path.expanduser("~/Data10/specES1/internal/"),
dir_path=os.path.expanduser("~/data/raw/logs/"),
setup="flomni",
use_vertical_default_values=True,
get_data_from_gui=False,
):
"""
Read the alignment parameters from xray eye fit.
"""
tomo_alignment_fit = np.zeros((2, 5))
# with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file:
# tomo_alignment_fit[0][0] = file.readline()
# with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file:
# tomo_alignment_fit[0][1] = file.readline()
if not get_data_from_gui:
"""
Read the alignment parameters from xray eye fit.
# with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file:
# tomo_alignment_fit[0][2] = file.readline()
"""
with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file:
tomo_alignment_fit[0][0] = file.readline()
# with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file:
# tomo_alignment_fit[1][0] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file:
tomo_alignment_fit[0][1] = file.readline()
# with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file:
# tomo_alignment_fit[1][1] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file:
tomo_alignment_fit[0][2] = file.readline()
# with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file:
# tomo_alignment_fit[1][2] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file:
tomo_alignment_fit[1][0] = file.readline()
# with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file:
# tomo_alignment_fit[1][3] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file:
tomo_alignment_fit[1][1] = file.readline()
# with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file:
# tomo_alignment_fit[1][4] = file.readline()
with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file:
tomo_alignment_fit[1][2] = file.readline()
params = dev.omny_xray_gui.fit_params_x.get()
with open(os.path.join(dir_path, "ptychotomoalign_Ay3.txt"), "r") as file:
tomo_alignment_fit[1][3] = file.readline()
#amplitude
tomo_alignment_fit[0][0] = params['SineModel_0_amplitude']
#phase
tomo_alignment_fit[0][1] = params['SineModel_0_shift']
#offset
tomo_alignment_fit[0][2] = params['LinearModel_1_intercept']
print("applying vertical default values from mirror calibration, not from fit!")
tomo_alignment_fit[1][0] = 0
tomo_alignment_fit[1][1] = 0
tomo_alignment_fit[1][2] = 0
tomo_alignment_fit[1][3] = 0
tomo_alignment_fit[1][4] = 0
with open(os.path.join(dir_path, "ptychotomoalign_Cy3.txt"), "r") as file:
tomo_alignment_fit[1][4] = file.readline()
print("New alignment parameters loaded from filesystem, meaning Matlab fit:")
else:
params = dev.omny_xray_gui.fit_params_x.get()
#amplitude
tomo_alignment_fit[0][0] = params['SineModel_0_amplitude']
#phase
tomo_alignment_fit[0][1] = params['SineModel_0_shift']
#offset
tomo_alignment_fit[0][2] = params['LinearModel_1_intercept']
print("applying vertical default values from mirror calibration, not from fit!")
tomo_alignment_fit[1][0] = 0
tomo_alignment_fit[1][1] = 0
tomo_alignment_fit[1][2] = 0
tomo_alignment_fit[1][3] = 0
tomo_alignment_fit[1][4] = 0
print("New alignment parameters loaded based on Xray eye alignment GUI:")
print("New alignment parameters loaded:")
print(
f"X Amplitude {tomo_alignment_fit[0][0]}, "
f"X Phase {tomo_alignment_fit[0][1]}, "
@@ -1429,18 +1454,6 @@ class Flomni(
def sample_name(self):
return self.sample_get_name(0)
def write_to_spec_log(self, content):
try:
with open(
os.path.expanduser(
"~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log"
),
"a",
) as log_file:
log_file.write(content)
except Exception:
logger.warning("Failed to write to spec log file (omny web page).")
def write_to_scilog(self, content, tags: list = None):
try:
if tags is not None:
@@ -1462,11 +1475,14 @@ class Flomni(
return
dev = builtins.__dict__.get("dev")
bec = builtins.__dict__.get("bec")
self.feye_out()
tags = ["BEC_alignment_tomo", self.sample_name]
self.write_to_scilog(
f"Starting alignment scan. First scan number: {bec.queue.next_scan_number}.", tags
)
start_angle = 0
angle_end = start_angle + 180
@@ -1495,9 +1511,8 @@ class Flomni(
for scan_nr in range(start_scan_number, end_scan_number):
self._write_tomo_scan_number(scan_nr, angle, 0)
print("Alignment scan finished. Please run SPEC_ptycho_align and load the new fit.")
umv(dev.fsamroy, 0)
self.OMNYTools.printgreenbold("\n\nAlignment scan finished. Please run SPEC_ptycho_align and load the new fit.")
def _write_subtomo_to_scilog(self, subtomo_number):
dev = builtins.__dict__.get("dev")
@@ -1531,6 +1546,9 @@ class Flomni(
self._write_subtomo_to_scilog(subtomo_number)
if start_angle is not None:
print(f"Sub tomo scan with start angle {start_angle} requested.")
if start_angle is None:
if subtomo_number == 1:
start_angle = 0
@@ -1549,29 +1567,94 @@ class Flomni(
elif subtomo_number == 8:
start_angle = self.tomo_angle_stepsize / 8.0 * 7
# _tomo_shift_angles (potential global variable)
_tomo_shift_angles = 0
angle_end = start_angle + 180
# compute number of projections
start = start_angle + _tomo_shift_angles
if subtomo_number % 2: # odd = forward
max_allowed_angle = 180.05 + self.tomo_angle_stepsize
proposed_end = start + 180
angle_end = min(proposed_end, max_allowed_angle)
span = angle_end - start
else: # even = reverse
min_allowed_angle = 0
proposed_end = start - 180
angle_end = max(proposed_end, min_allowed_angle)
span = start - angle_end
# number of projections needed to maintain step size
N = int(span / self.tomo_angle_stepsize) + 1
angles = np.linspace(
start_angle + _tomo_shift_angles,
start,
angle_end,
num=int(180 / self.tomo_angle_stepsize) + 1,
num=N,
endpoint=True,
)
# reverse even sub-tomograms
if not (subtomo_number % 2):
angles = np.flip(angles)
for angle in angles:
if subtomo_number % 2: # odd subtomos → forward direction
# clamp end angle to max allowed
max_allowed_angle = 180.05 + self.tomo_angle_stepsize
proposed_end = start + 180
angle_end = min(proposed_end, max_allowed_angle)
angles = np.linspace(
start,
angle_end,
num=N,
endpoint=True,
)
else: # even subtomos → reverse direction
# go FROM start_angle down toward 0
min_allowed_angle = 0
proposed_end = start - 180
angle_end = max(proposed_end, min_allowed_angle)
angles = np.linspace(
start,
angle_end,
num=N,
endpoint=True,
)
for i, angle in enumerate(angles):
self.progress["subtomo"] = subtomo_number
self.progress["subtomo_projection"] = np.where(angles == angle)[0][0]
self.progress["subtomo_total_projections"] = 180 / self.tomo_angle_stepsize
self.progress["projection"] = (subtomo_number - 1) * self.progress[
"subtomo_total_projections"
] + self.progress["subtomo_projection"]
# --- NEW LOGIC FOR OFFSET WHEN start_angle IS SPECIFIED ---
if i == 0:
step = self.tomo_angle_stepsize
sa = start_angle
if start_angle is None:
# normal operation: always start at zero
self._subtomo_offset = 0
else:
if subtomo_number % 2: # odd = forward direction
self._subtomo_offset = round(sa / step)
else: # even = reverse direction
self._subtomo_offset = round((180 - sa) / step)
# progress index must always increase
self.progress["subtomo_projection"] = self._subtomo_offset + i
# ------------------------------------------------------------
# existing progress fields
self.progress["subtomo_total_projections"] = int(180 / self.tomo_angle_stepsize)
self.progress["projection"] = (subtomo_number - 1) * self.progress["subtomo_total_projections"] + self.progress["subtomo_projection"]
self.progress["total_projections"] = 180 / self.tomo_angle_stepsize * 8
self.progress["angle"] = angle
# finally do the scan at this angle
self._tomo_scan_at_angle(angle, subtomo_number)
def _tomo_scan_at_angle(self, angle, subtomo_number):
successful = False
error_caught = False
@@ -1579,7 +1662,7 @@ class Flomni(
print(f"Starting flOMNI scan for angle {angle} in subtomo {subtomo_number}")
self._print_progress()
while not successful:
self.bl_chk._bl_chk_start()
#self.bl_chk._bl_chk_start()
if not self.special_angles:
self._current_special_angles = []
if self._current_special_angles:
@@ -1602,10 +1685,10 @@ class Flomni(
else:
raise exc
if self.bl_chk._bl_chk_stop() and not error_caught:
successful = True
else:
self.bl_chk._bl_chk_wait_until_recovered()
# if self.bl_chk._bl_chk_stop() and not error_caught:
successful = True
# else:
# self.bl_chk._bl_chk_wait_until_recovered()
end_scan_number = bec.queue.next_scan_number
for scan_nr in range(start_scan_number, end_scan_number):
self._write_tomo_scan_number(scan_nr, angle, subtomo_number)
@@ -1613,6 +1696,14 @@ class Flomni(
def tomo_scan(self, subtomo_start=1, start_angle=None, projection_number=None):
"""start a tomo scan"""
if not self._check_eye_out_and_optics_in():
print("Attention: The setup is not in measurement condition.\nXray eye might be IN or the Xray optics OUT.")
if self.OMNYTools.yesno("Shall I continue?", "n"):
print("OK")
else:
print("Stopping.")
return
self.flomnigui_show_progress()
bec = builtins.__dict__.get("bec")
@@ -1626,19 +1717,19 @@ class Flomni(
):
# pylint: disable=undefined-variable
if bec.active_account != "":
self.tomo_id = self.add_sample_database(
self.sample_name,
str(datetime.date.today()),
bec.active_account.decode(),
bec.queue.next_scan_number,
"flomni",
"test additional info",
"BEC",
)
self.write_pdf_report()
else:
self.tomo_id = 0
# if bec.active_account != "":
# self.tomo_id = self.add_sample_database(
# self.sample_name,
# str(datetime.date.today()),
# bec.active_account,
# bec.queue.next_scan_number,
# "flomni",
# "test additional info",
# "BEC",
# )
# self.write_pdf_report()
# else:
self.tomo_id = 0
with scans.dataset_id_on_hold:
if self.tomo_type == 1:
@@ -1647,7 +1738,7 @@ class Flomni(
for ii in range(subtomo_start, 9):
self.sub_tomo_scan(ii, start_angle=start_angle)
start_angle = None
elif self.tomo_type == 2:
# Golden ratio tomography
previous_subtomo_number = -1
@@ -1743,6 +1834,11 @@ class Flomni(
else:
raise FlomniError("undefined tomo type")
self.progress['projection'] = self.progress['total_projections']
self.progress["subtomo_projection"] = self.progress["subtomo_total_projections"]
self._print_progress()
self.OMNYTools.printgreenbold("Tomoscan finished")
def _print_progress(self):
print("\x1b[95mProgress report:")
print(f"Tomo type: ....................... {self.progress['tomo_type']}")
@@ -1824,7 +1920,7 @@ class Flomni(
return angle, subtomo_number
def tomo_reconstruct(self, base_path="~/Data10/specES1"):
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(
@@ -1835,7 +1931,7 @@ class Flomni(
def _write_tomo_scan_number(self, scan_number: int, angle: float, subtomo_number: int) -> None:
tomo_scan_numbers_file = os.path.expanduser(
"~/tomography_scannumbers.txt"
"~/data/raw/logs/tomography_scannumbers.txt"
)
with open(tomo_scan_numbers_file, "a+") as out_file:
# pylint: disable=undefined-variable
@@ -1844,6 +1940,10 @@ class Flomni(
)
def tomo_scan_projection(self, angle: float):
dev.rtx.controller.laser_tracker_check_signalstrength()
scans = builtins.__dict__.get("scans")
# additional_correction = self.align.compute_additional_correction(angle)
@@ -1878,7 +1978,6 @@ class Flomni(
f"{str(datetime.datetime.now())}: flomni scan projection at angle {angle}, scan"
f" number {bec.queue.next_scan_number}.\n"
)
self.write_to_spec_log(log_message)
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
scans.flomni_fermat_scan(
fovx=self.fovx,

View File

@@ -1,10 +1,10 @@
import time
import numpy as np
from rich import box
from rich.console import Console
from rich.table import Table
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put, fshclose
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_put
class FlomniOpticsMixin:
@@ -16,12 +16,18 @@ class FlomniOpticsMixin:
return param.get(var)
def feye_out(self):
fshclose()
dev.omnyfsh.fshclose()
self.foptics_in()
self.flomnigui_show_xeyealign()
self.xrayeye_update_frame()
if self.OMNYTools.yesno("Did the direct beam on the xray eye disappear?"):
print("excellent.")
else:
print("Aborting. With visible parts of the direct beam on the xray eye, it cannot be removed.")
return
feyex_out = self._get_user_param_safe("feyex", "out")
umv(dev.feyex, feyex_out)
epics_put("XOMNYI-XEYE-ACQ:0", 2)
# move rotation stage to zero to avoid problems with wires
umv(dev.fsamroy, 0)
# umv(dev.fttrx1, 9.2)
@@ -32,16 +38,39 @@ class FlomniOpticsMixin:
feyex_in = self._get_user_param_safe("feyex", "in")
feyey_in = self._get_user_param_safe("feyey", "in")
umv(dev.feyex, feyex_in, dev.feyey, feyey_in)
#self.align.update_frame()
current_feyex = dev.feyex.readback.get()
current_feyey = dev.feyey.readback.get()
# check if both are close enough (within 0.01)
if np.isclose(current_feyex, feyex_in, atol=0.01) and np.isclose(current_feyey, feyey_in, atol=0.01):
# both already in position → do nothing
pass
else:
# move both axes to the desired "in" positions
umv(dev.feyex, feyex_in, dev.feyey, feyey_in)
self.xrayeye_update_frame()
def _ffzp_in(self):
foptx_in = self._get_user_param_safe("foptx", "in")
fopty_in = self._get_user_param_safe("fopty", "in")
umv(dev.foptx, foptx_in)
umv(
dev.fopty, fopty_in
) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
current_foptx = dev.foptx.readback.get()
current_fopty = dev.fopty.readback.get()
tol = 0.003
# if either axis is outside the tolerance → move both
need_move_optics = (
not np.isclose(current_foptx, foptx_in, atol=tol) or
not np.isclose(current_fopty, fopty_in, atol=tol)
)
if need_move_optics:
umv(dev.foptx, foptx_in, dev.fopty, fopty_in) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm
else:
print("FZP is already at the in position.")
def ffzp_in(self):
"""
@@ -84,19 +113,85 @@ class FlomniOpticsMixin:
# umv(dev.losax, -1.4850, dev.losay, -0.1930)
# umv(dev.losaz, 1.0000)
# 7.2, 150
fosax_in = self._get_user_param_safe("fosax", "in")
fosay_in = self._get_user_param_safe("fosay", "in")
fosaz_in = self._get_user_param_safe("fosaz", "in")
# tighten limits
dev.fosax.limits = [fosax_in - 0.1, fosax_in + 0.1]
dev.fosay.limits = [fosay_in - 0.1, fosay_in + 0.1]
dev.fosaz.limits = [fosaz_in - 0.1, fosaz_in + 0.1]
umv(dev.fosax, fosax_in, dev.fosay, fosay_in)
umv(dev.fosaz, fosaz_in)
current_fosax = dev.fosax.readback.get()
current_fosay = dev.fosay.readback.get()
current_fosaz = dev.fosaz.readback.get()
# tolerance
tol = 0.003
need_move_osa = (
not np.isclose(current_fosax, fosax_in, atol=tol) or
not np.isclose(current_fosay, fosay_in, atol=tol) or
not np.isclose(current_fosaz, fosaz_in, atol=tol)
)
if need_move_osa:
umv(dev.fosax, fosax_in, dev.fosay, fosay_in)
umv(dev.fosaz, fosaz_in)
else:
print("OSA is already at the IN position.")
# 11 kev
# umv(dev.losax, -1.161000, dev.losay, -0.196)
# umv(dev.losaz, 1.0000)
def _check_eye_out_and_optics_in(self, tol=0.003):
# --- expected IN positions ---
foptx_in = self._get_user_param_safe("foptx", "in")
fopty_in = self._get_user_param_safe("fopty", "in")
foptz_in = self._get_user_param_safe("foptz", "in")
# --- expected OUT condition for the X-ray eye ---
# eye is OUT when it is *not within tolerance* of its IN position
feyex_out = self._get_user_param_safe("feyex", "out")
# --- current positions ---
cx_feyex = dev.feyex.readback.get()
cx_foptx = dev.foptx.readback.get()
cx_fopty = dev.fopty.readback.get()
cx_foptz = dev.foptz.readback.get()
# --- check eye OUT ---
eye_out = (
np.isclose(cx_feyex, feyex_out, atol=tol)
)
# --- check optics IN ---
optics_in = (
np.isclose(cx_foptx, foptx_in, atol=tol) and
np.isclose(cx_fopty, fopty_in, atol=tol) and
np.isclose(cx_foptz, foptz_in, atol=tol)
)
fosax_in = self._get_user_param_safe("fosax", "in")
fosay_in = self._get_user_param_safe("fosay", "in")
fosaz_in = self._get_user_param_safe("fosaz", "in")
cx_fosax = dev.fosax.readback.get()
cx_fosay = dev.fosay.readback.get()
cx_fosaz = dev.fosaz.readback.get()
osa_in = (
np.isclose(cx_fosax, fosax_in, atol=tol) and
np.isclose(cx_fosay, fosay_in, atol=tol) and
np.isclose(cx_fosaz, fosaz_in, atol=tol)
)
return eye_out and optics_in and osa_in
def fosa_out(self):
self.ensure_fheater_up()
curtain_is_triggered = dev.foptz.controller.fosaz_light_curtain_is_triggered()

View File

@@ -1,4 +1,5 @@
import builtins
import time
# from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get, epics_put, fshopen, fshclose
@@ -31,6 +32,7 @@ class flomniGuiTools:
self.gui.flomni.raise_window()
else:
self.gui.new("flomni")
time.sleep(1)
def flomnigui_stop_gui(self):
self.gui.flomni.hide()
@@ -57,15 +59,22 @@ class flomniGuiTools:
def _flomnigui_check_attribute_not_exists(self, attribute_name):
if hasattr(self.gui, "flomni"):
if hasattr(self.gui.flomni, attribute_name):
return False
if attribute_name == "xeyegui":
if hasattr(self.gui.flomni, "xrayeye"):
return False
if attribute_name == "progressbar":
if hasattr(self.gui.flomni, "RingProgressBar"):
return False
if attribute_name == "cam_flomni_gripper" or attribute_name == "cam_flomni_overview":
if hasattr(self.gui.flomni, "Image") or hasattr(self.gui.flomni, "Image_0"):
return False
return True
def flomnigui_show_cameras(self):
self.flomnigui_show_gui()
if self._flomnigui_check_attribute_not_exists(
"camera_gripper"
) or self._flomnigui_check_attribute_not_exists("camera_overview"):
"cam_flomni_gripper"
) or self._flomnigui_check_attribute_not_exists("cam_flomni_overview"):
self.flomnigui_remove_all_docks()
camera_gripper_image = self.gui.flomni.new("Image")
if self._flomnicam_check_device_exists(dev.cam_flomni_gripper):
@@ -94,7 +103,8 @@ class flomniGuiTools:
# dev.cam_flomni_overview.stop_live_mode()
# dev.cam_flomni_gripper.stop_live_mode()
# dev.cam_xeye.live_mode = False
self.gui.flomni.delete_all()
if hasattr(self.gui, "flomni"):
self.gui.flomni.delete_all()
self.progressbar = None
self.text_box = None
@@ -200,7 +210,16 @@ class flomniGuiTools:
main_progress_ring.set_value(progress)
subtomo_progress_ring.set_value(subtomo_progress)
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']}"
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'])}"
)
self.progressbar.set_center_label(text)

View File

@@ -49,6 +49,8 @@ class XrayEyeAlign:
self._xray_fov_xy = [0, 0]
def update_frame(self, keep_shutter_open=False):
if self.flomni._flomnigui_check_attribute_not_exists("xeyegui"):
self.flomni.flomnigui_show_xeyealign()
if not dev.cam_xeye.live_mode_enabled.get():
dev.cam_xeye.live_mode_enabled.put(True)
@@ -229,10 +231,6 @@ class XrayEyeAlign:
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)
if keep_shutter_open:
if self.flomni.OMNYTools.yesno("Close the shutter now?", "y"):
dev.omnyfsh.fshclose()
@@ -245,7 +243,16 @@ class XrayEyeAlign:
)
print("Check the fit in the GUI...")
print("Then LOAD ALIGNMENT PARAMETERS by running flomni.read_alignment_offset()\n")
time.sleep(5)
print("Automatically loading new alignment parameters from xray eye alignment.\n")
self.flomni.read_alignment_offset(get_data_from_gui=True)
self.tomo_rotate(0)
umv(dev.rtx, 0)
print("You are ready to remove the xray eye and start ptychography scans.")
def write_output(self):
file = os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues")
@@ -266,7 +273,7 @@ class XrayEyeAlign:
fovx_x = (k - 1) * 45
fovx_list.append([fovx_x, fovx_offset * 1000]) # Append the data to the list
print(f"Writing to file new alignment: number {k}, value x {fovx_offset}")
print(f"Alignment number {k}, value x {fovx_offset}")
alignment_values_file.write(f"{fovx_x}\t{fovx_offset * 1000}\n")
# Now build final numpy array:

View File

@@ -852,18 +852,6 @@ class OMNY(
def sample_name(self):
return dev.omny_samples.get_sample_name_in_samplestage()
def write_to_spec_log(self, content):
try:
with open(
os.path.expanduser(
"~/Data10/specES1/log-files/specES1_started_2022_11_30_1313.log"
),
"a",
) as log_file:
log_file.write(content)
except Exception:
logger.warning("Failed to write to spec log file (omny web page).")
def write_to_scilog(self, content, tags: list = None):
try:
if tags is not None:
@@ -1288,7 +1276,6 @@ class OMNY(
f"{str(datetime.datetime.now())}: omny scan projection at angle {angle}, scan"
f" number {bec.queue.next_scan_number}.\n"
)
self.write_to_spec_log(log_message)
# self.write_to_scilog(log_message, ["BEC_scans", self.sample_name])
scans.omny_fermat_scan(
fovx=self.fovx,

View File

@@ -48,7 +48,7 @@ class OMNYOpticsMixin:
dev.oeyez.controller.socket_put_confirmed("axspeed[7]=10000")
def oeye_out(self):
fshclose()
dev.omnyfsh.fshclose()
if self.OMNYTools.yesno("Did you move in the optics?"):
umv(dev.oeyez, -2)
self._oeyey_mv(-60.3)

View File

@@ -25,7 +25,6 @@ from qtpy.QtWidgets import (
QTextEdit,
QTabWidget,
)
import time
logger = bec_logger.logger
CAMERA = ("cam_xeye", "image")
@@ -146,6 +145,7 @@ class XRayEye(BECWidget, QWidget):
def __init__(self, parent=None, **kwargs):
super().__init__(parent=parent, **kwargs)
self._connected_motor = None
self.get_bec_shortcuts()
self._init_ui()
@@ -357,13 +357,14 @@ class XRayEye(BECWidget, QWidget):
self.on_tomo_angle_readback, MessageEndpoints.device_readback(motor)
)
logger.info(f"Successfully connected to {motor}")
self._connected_motor = motor
################################################################################
# Properties ported from the original OmnyAlignment, can be adjusted as needed
################################################################################
@SafeProperty(str)
def user_message(self):
return self.message_line_edit.text()
return self.message_line_edit.toPlainText()
@user_message.setter
def user_message(self, message: str):
@@ -520,44 +521,50 @@ class XRayEye(BECWidget, QWidget):
@SafeSlot()
def submit(self):
"""Execute submit action by submit button."""
print("submit pushed")
self.submit_button.blockSignals(True)
if self.roi_manager.single_active_roi is None:
logger.warning("No active ROI")
return
roi_coordinates = self.roi_manager.single_active_roi.get_coordinates()
roi_center_x = roi_coordinates["center_x"]
roi_center_y = roi_coordinates["center_y"]
# Case of rectangular ROI
if isinstance(self.roi_manager.single_active_roi, RectangularROI):
roi_width = roi_coordinates["width"]
roi_height = roi_coordinates["height"]
elif isinstance(self.roi_manager.single_active_roi, CircularROI):
roi_width = roi_coordinates["diameter"]
roi_height = roi_coordinates["radius"]
else:
logger.warning("Unsupported ROI type for submit action.")
return
try:
if self.roi_manager.single_active_roi is None:
logger.warning("No active ROI")
return
roi_coordinates = self.roi_manager.single_active_roi.get_coordinates()
roi_center_x = roi_coordinates["center_x"]
roi_center_y = roi_coordinates["center_y"]
# Case of rectangular ROI
if isinstance(self.roi_manager.single_active_roi, RectangularROI):
roi_width = roi_coordinates["width"]
roi_height = roi_coordinates["height"]
elif isinstance(self.roi_manager.single_active_roi, CircularROI):
roi_width = roi_coordinates["diameter"]
roi_height = roi_coordinates["radius"]
else:
logger.warning("Unsupported ROI type for submit action.")
return
print(
f"current roi: x:{roi_center_x}, y:{roi_center_y}, w:{roi_width},h:{roi_height}"
) # TODO remove when will be not needed for debugging
# submit roi coordinates
step = int(self.dev.omny_xray_gui.step.read().get("omny_xray_gui_step").get("value"))
# submit roi coordinates
step = int(self.dev.omny_xray_gui.step.read().get("omny_xray_gui_step").get("value"))
xval_x = getattr(self.dev.omny_xray_gui, f"xval_x_{step}").set(roi_center_x)
xval_y = getattr(self.dev.omny_xray_gui, f"yval_y_{step}").set(roi_center_y)
width_x = getattr(self.dev.omny_xray_gui, f"width_x_{step}").set(roi_width)
width_y = getattr(self.dev.omny_xray_gui, f"width_y_{step}").set(roi_height)
self.dev.omny_xray_gui.submit.set(1)
print("submit done")
self.submit_button.blockSignals(False)
getattr(self.dev.omny_xray_gui, f"xval_x_{step}").set(roi_center_x)
getattr(self.dev.omny_xray_gui, f"yval_y_{step}").set(roi_center_y)
getattr(self.dev.omny_xray_gui, f"width_x_{step}").set(roi_width)
getattr(self.dev.omny_xray_gui, f"width_y_{step}").set(roi_height)
self.dev.omny_xray_gui.submit.set(1)
finally:
self.submit_button.blockSignals(False)
def cleanup(self):
"""Cleanup connections on widget close -> disconnect slots and stop live mode of camera."""
if self._connected_motor is not None:
self.bec_dispatcher.disconnect_slot(
self.on_tomo_angle_readback, MessageEndpoints.device_readback(self._connected_motor)
)
self.bec_dispatcher.disconnect_slot(
self.device_updates, MessageEndpoints.device_readback("omny_xray_gui")
self.getting_shutter_status, MessageEndpoints.device_readback("omnyfsh")
)
self.bec_dispatcher.disconnect_slot(
self.getting_camera_status, MessageEndpoints.device_read_configuration(CAMERA[0])
)
getattr(self.dev, CAMERA[0]).stop_live_mode()
super().cleanup()

View File

@@ -227,7 +227,7 @@ ftransy:
readoutPriority: baseline
connectionTimeout: 20
userParameter:
sensor_voltage: -2.4
sensor_voltage: -1.1
ftransz:
description: Sample transer Z
deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor
@@ -344,6 +344,9 @@ rtx:
description: flomni rt
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
limits:
- -200
- 200
axis_Id: A
host: mpc2844.psi.ch
port: 2222
@@ -361,6 +364,9 @@ rty:
description: flomni rt
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
limits:
- -100
- 100
axis_Id: B
host: mpc2844.psi.ch
port: 2222
@@ -376,6 +382,9 @@ rtz:
description: flomni rt
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniMotor
deviceConfig:
limits:
- -100
- 100
axis_Id: C
host: mpc2844.psi.ch
port: 2222

View File

@@ -104,7 +104,7 @@ DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
("B", CHANNELREFERENCE.A),
("C", CHANNELREFERENCE.T0), # T0
("D", CHANNELREFERENCE.C),
("E", CHANNELREFERENCE.D), # D One extra pulse once shutter closes for MCS
("E", CHANNELREFERENCE.B), # B One extra pulse once shutter closes for MCS
("F", CHANNELREFERENCE.E), # E + 1mu s
("G", CHANNELREFERENCE.T0),
("H", CHANNELREFERENCE.G),
@@ -213,8 +213,23 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# NOTE Burst delay should be set to 0, don't remove as this will not be checked
# Also set the burst count to 1 to only have a single pulse for DDG1.
# As the IOC may be out of sync with the HW, we make sure that we set the default parameters
# in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out
# of sync.
self.burst_delay.put(1)
time.sleep(0.02) # Give HW time to process
self.burst_delay.put(0)
time.sleep(0.02)
self.burst_count.put(2)
time.sleep(0.02)
self.burst_count.put(1)
time.sleep(0.02)
self.burst_mode.put(1)
time.sleep(0.02)
self.burst_mode.put(0)
time.sleep(0.02)
def keep_shutter_open_during_scan(self, open: True) -> None:
"""
@@ -291,17 +306,24 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# Burst Period DDG1
# Set burst_period to shutter width
# c/t0 + self._shutter_to_open_delay + exp_time * burst_count
shutter_width = (
self._shutter_to_open_delay + exp_time * frames_per_trigger
) # Shutter starts closing at end of exposure
# SHUTTER WIDTH timing consists of the delay for the shutter to open
# + the exposure time * frames per trigger
shutter_width = self._shutter_to_open_delay + exp_time * frames_per_trigger
# TOTAL EXPOSURE accounts for the shutter to open AND close. In addition, we add
# a short additional delay of 3e-6 to allow for the extra trigger through 'ef'
# (delay of 1e-6, width of 1e-6)
total_exposure_time = 2 * self._shutter_to_open_delay + exp_time * frames_per_trigger + 3e-6
if self.burst_period.get() != shutter_width:
self.burst_period.put(shutter_width)
# The burst_period has to be slightly longer
self.burst_period.put(total_exposure_time)
# Trigger DDG2
# a = t0 + 2ms, b = a + 1us
# a has reference to t0, b has reference to a
# Add delay of self._shutter_to_open_delay to allow shutter to open
self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=1e-6)
# AB is delayed by the shutter opening time, and the falling edge indicates the shutter has
# fully closed, it has to be considered as the blocking signal for the next acquisition to start.
# PS: + 3e-6
self.set_delay_pairs(channel="ab", delay=self._shutter_to_open_delay, width=shutter_width)
# Trigger shutter
# d = c/t0 + self._shutter_to_open_delay + exp_time * burst_count + 1ms
@@ -321,7 +343,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
if self.scan_info.msg.scan_type == "fly":
self.set_delay_pairs(channel="ef", delay=0, width=0)
else:
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
self.set_delay_pairs(channel="ef", delay=1e-6, width=1e-6)
# NOTE Add additional sleep to make sure that the IOC and DDG HW process the values properly
# This value has been choosen empirically after testing with the HW. It's

View File

@@ -29,6 +29,7 @@ from ophyd_devices import DeviceStatus, StatusBase
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
BURSTCONFIG,
CHANNELREFERENCE,
OUTPUTPOLARITY,
STATUSBITS,
@@ -37,7 +38,6 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
ChannelConfig,
DelayGeneratorCSAXS,
LiteralChannels,
BURSTCONFIG,
)
logger = bec_logger.logger
@@ -138,6 +138,24 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
# Set burst config
self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value)
# TODO As the IOC may be out of sync with the HW, we make sure that we set the default parameters
# in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out
# of sync.
self.burst_delay.put(1)
time.sleep(0.02) # Give HW time to process
self.burst_delay.put(0)
time.sleep(0.02)
self.burst_count.put(2)
time.sleep(0.02)
self.burst_count.put(1)
time.sleep(0.02)
self.burst_mode.put(1)
time.sleep(0.02)
self.burst_mode.put(0)
time.sleep(0.02)
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""

View File

@@ -20,6 +20,7 @@ from typing import TYPE_CHECKING, Callable, Literal
import numpy as np
from bec_lib.logger import bec_logger
from ophyd.utils.errors import WaitTimeoutError
from ophyd import Component as Cpt
from ophyd import EpicsSignalRO, Kind
from ophyd_devices import (
@@ -513,7 +514,22 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
# that the acquisition finishes on the card and that data is emitted to BEC. If the acquisition
# was already finished (i.e. normal step scan sends 1 extra pulse per burst cycle), this will
# not have any effect as the card will already be in DONE state and signal.
self.software_channel_advance.put(1)
if self.scan_info.msg.scan_type == "fly":
expected_points = int(
self.scan_info.msg.num_points
* self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
)
status = CompareStatus(self.current_channel, expected_points-1, operation_success=">=")
try:
status.wait(timeout=5)
except WaitTimeoutError:
text = f"Device {self.name} received num points {self.current_channel.get()} / {expected_points}. Device timed out after 5s."
logger.error(text)
raise TimeoutError(text)
# Manually set the last advance
self.software_channel_advance.put(1)
# Prepare and register status callback for the async monitoring loop
status_async_data = StatusBase(obj=self)

View File

@@ -92,7 +92,8 @@ class RtFlomniController(Controller):
parent._min_scan_buffer_reached = False
start_time = time.time()
for pos_index, pos in enumerate(positions):
parent.socket_put_and_receive(f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}")
cmd = f"s{pos[0]:.05f},{pos[1]:.05f},{pos[2]:.05f}"
parent.socket_put_and_receive(cmd)
if pos_index > 100:
parent._min_scan_buffer_reached = True
parent._min_scan_buffer_reached = True
@@ -174,8 +175,12 @@ class RtFlomniController(Controller):
self.set_device_read_write("foptx", False)
self.set_device_read_write("fopty", False)
def move_samx_to_scan_region(self, fovx: float, cenx: float):
def move_samx_to_scan_region(self, cenx: float, move_in_this_routine: bool = False):
# attention. a movement will clear all positions in the rt trajectory generator!
if move_in_this_routine == True:
self.device_manager.devices.rtx.obj.move(cenx, wait=True)
time.sleep(0.05)
# at cenx we expect the PID to be close to zero for a good fsamx position
if self.rt_pid_voltage is None:
rtx = self.device_manager.devices.rtx
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
@@ -184,31 +189,41 @@ class RtFlomniController(Controller):
"rt_pid_voltage not set in rtx user parameters. Please run feedback_enable_with_reset first."
)
logger.info(f"Using PID voltage from rtx user parameter: {self.rt_pid_voltage}")
expected_voltage = self.rt_pid_voltage + fovx / 2 * 7 / 100
logger.info(f"Expected PID voltage: {expected_voltage}")
expected_voltage = self.rt_pid_voltage
# logger.info(f"Expected PID voltage: {expected_voltage}")
logger.info(f"Current PID voltage: {self.get_pid_x()}")
wait_on_exit = False
while True:
if np.abs(self.get_pid_x() - expected_voltage) < 1:
break
wait_on_exit = True
self.socket_put("v0")
# we allow 2V range from center, this corresponds to 30 microns
if np.abs(self.get_pid_x() - expected_voltage) < 2:
logger.info("No correction of fsamx needed")
else:
fsamx = self.device_manager.devices.fsamx
fsamx.read_only = False
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007
logger.info(f"Correcting fsamx by {fsamx.obj.pid_x_correction}")
fsamx_in = fsamx.user_parameter.get("in")
fsamx.obj.move(fsamx_in + cenx / 1000 + fsamx.obj.pid_x_correction, wait=True)
fsamx.read_only = True
time.sleep(0.1)
self.laser_tracker_on()
time.sleep(0.01)
while True:
# when we correct, then to 1 V, within 15 microns
if np.abs(self.get_pid_x() - expected_voltage) < 1:
logger.info("No further correction needed")
break
wait_on_exit = True
# disable FZP piezo feedback
self.socket_put("v0")
fsamx.read_only = False
logger.info(f"Current PID voltage: {self.get_pid_x()}")
# here we accumulate the correction
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.006
fsamx_in = fsamx.user_parameter.get("in")
logger.info(
f"Moving fsamx to {cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction}, PID portion of that {fsamx.obj.pid_x_correction}"
)
fsamx.obj.move(fsamx_in + cenx / 1000 * 0.7 + fsamx.obj.pid_x_correction, wait=True)
fsamx.read_only = True
time.sleep(0.1)
self.laser_tracker_on()
time.sleep(0.01)
if wait_on_exit:
time.sleep(1)
# enable fast FZP feedback again
self.socket_put("v1")
@threadlocked
@@ -379,7 +394,7 @@ class RtFlomniController(Controller):
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
return val
def laser_tracker_check_signalstrength(self):
def laser_tracker_check_signalstrength(self, verbose=True):
if not self.laser_tracker_check_enabled():
returnval = "disabled"
else:
@@ -390,9 +405,10 @@ class RtFlomniController(Controller):
rtx = self.device_manager.devices.rtx
min_signal = rtx.user_parameter.get("min_signal")
low_signal = rtx.user_parameter.get("low_signal")
print(f"low signal: {low_signal}")
print(f"min signal: {min_signal}")
print(f"signal: {signal}")
if verbose:
print(f"low signal: {low_signal}")
print(f"min signal: {min_signal}")
print(f"signal: {signal}")
if signal < min_signal:
time.sleep(1)
if signal < min_signal:
@@ -499,7 +515,7 @@ class RtFlomniController(Controller):
# while scan is running
while mode > 0:
#TODO here?: scan abortion if no progress in scan *raise error
# TODO here?: scan abortion if no progress in scan *raise error
# logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
mode, number_of_positions_planned, current_position_in_scan = self.get_scan_status()
@@ -606,6 +622,18 @@ class RtFlomniSetpointSignal(RtSetpointSignal):
"The interferometer feedback is not running. Either it is turned off or and"
" interferometer error occured."
)
tracker_status = self.parent.controller.laser_tracker_check_signalstrength()
if tracker_status == "toolow":
print(
"The interferometer signal is too low for movements. Realignment required."
)
raise RtError(
"The interferometer signal is too low for movements. Realignment required."
)
self.set_with_feedback_disabled(val)
def set_with_feedback_disabled(self, val):

View File

@@ -48,6 +48,7 @@ class OMNYFastShutter(PSIDeviceBase, Device):
def fshopen(self):
"""Open the fast shutter."""
if self._check_if_cSAXS_shutter_exists_in_config():
self.shutter.put(1)
return self.device_manager.devices["fsh"].fshopen()
else:
self.shutter.put(1)
@@ -55,6 +56,7 @@ class OMNYFastShutter(PSIDeviceBase, Device):
def fshclose(self):
"""Close the fast shutter."""
if self._check_if_cSAXS_shutter_exists_in_config():
self.shutter.put(0)
return self.device_manager.devices["fsh"].fshclose()
else:
self.shutter.put(0)

View File

@@ -24,9 +24,11 @@ import time
import numpy as np
from bec_lib import bec_logger, messages
from bec_lib.alarm_handler import Alarms
from bec_lib.endpoints import MessageEndpoints
from bec_server.scan_server.errors import ScanAbortion
from bec_server.scan_server.scans import SyncFlyScanBase
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import TRIGGERSOURCE
logger = bec_logger.logger
@@ -52,7 +54,7 @@ class FlomniFermatScan(SyncFlyScanBase):
angle: float = None,
corridor_size: float = 3,
parameter: dict = None,
frames_per_trigger:int=1,
frames_per_trigger: int = 1,
**kwargs,
):
"""
@@ -76,7 +78,9 @@ class FlomniFermatScan(SyncFlyScanBase):
>>> scans.flomni_fermat_scan(fovx=20, fovy=25, cenx=0.02, ceny=0, zshift=0, angle=0, step=0.5, exp_time=0.01, frames_per_trigger=1)
"""
super().__init__(parameter=parameter, exp_time=exp_time, frames_per_trigger=frames_per_trigger, **kwargs)
super().__init__(
parameter=parameter, exp_time=exp_time, frames_per_trigger=frames_per_trigger, **kwargs
)
self.show_live_table = False
self.axis = []
self.fovx = fovx
@@ -153,13 +157,16 @@ class FlomniFermatScan(SyncFlyScanBase):
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
def _prepare_setup_part2(self):
# Prepare DDG1 to use
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value)
# Prepare DDG1 to use
yield from self.stubs.send_rpc_and_wait(
"ddg1", "set_trigger", TRIGGERSOURCE.EXT_RISING_EDGE.value
)
if self.flomni_rotation_status:
self.flomni_rotation_status.wait()
rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
# rtx_status = yield from self.stubs.set(device="rtx", value=self.positions[0][0], wait=False)
rtx_status = yield from self.stubs.set(device="rtx", value=self.cenx, wait=False)
rtz_status = yield from self.stubs.set(device="rtz", value=self.positions[0][2], wait=False)
yield from self.stubs.send_rpc_and_wait("rtx", "controller.laser_tracker_on")
@@ -167,22 +174,24 @@ class FlomniFermatScan(SyncFlyScanBase):
rtx_status.wait()
rtz_status.wait()
# status = yield from self.stubs.send_rpc("rtx", "move", self.cenx)
# status.wait()
yield from self._transfer_positions_to_flomni()
yield from self.stubs.send_rpc_and_wait(
"rtx", "controller.move_samx_to_scan_region", self.fovx, self.cenx
)
tracker_signal_status = yield from self.stubs.send_rpc_and_wait(
"rtx", "controller.laser_tracker_check_signalstrength"
)
#self.device_manager.connector.send_client_info(tracker_signal_status)
yield from self.stubs.send_rpc_and_wait(
"rtx", "controller.move_samx_to_scan_region", self.cenx
)
# self.device_manager.connector.send_client_info(tracker_signal_status)
if tracker_signal_status == "low":
self.device_manager.connector.raise_alarm(
severity=0,
alarm_type="LaserTrackerSignalStrength",
source={"device": "rtx", "reason": "low signal strength", "method": "_prepare_setup_part2"},
metadata={},
msg="Signal strength of the laser tracker is low, sufficient to continue. Realignment recommended!",
error_info = messages.ErrorInfo(
error_message="Signal strength of the laser tracker is low, but sufficient to continue. Realignment recommended!",
compact_error_message="Low signal strength of the laser tracker. Realignment recommended!",
exception_type="LaserTrackerSignalStrengthLow",
device="rtx",
)
self.device_manager.connector.raise_alarm(severity=Alarms.WARNING, info=error_info)
elif tracker_signal_status == "toolow":
raise ScanAbortion(
"Signal strength of the laser tracker is too low for scanning. Realignment required!"
@@ -307,13 +316,19 @@ class FlomniFermatScan(SyncFlyScanBase):
# in flomni, we need to move to the start position of the next scan, which is the end position of the current scan
# this method is called in finalize and overwrites the default move_to_start()
if isinstance(self.positions, np.ndarray) and len(self.positions[-1]) == 3:
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
# yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=self.positions[-1])
# in x we move to cenx, then we avoid jumps in centering routine
value = self.positions[-1]
value[0] = self.cenx
yield from self.stubs.set(device=["rtx", "rty", "rtz"], value=value)
return
logger.warning("No positions found to return to start")
def cleanup(self):
yield from self.stubs.send_rpc_and_wait("ddg1", "set_trigger", TRIGGERSOURCE.SINGLE_SHOT.value)
yield from self.stubs.send_rpc_and_wait(
"ddg1", "set_trigger", TRIGGERSOURCE.SINGLE_SHOT.value
)
yield from super().cleanup()
def run(self):

View File

@@ -50,16 +50,15 @@ Manually move the gripper to a transfer position
After the sample transfer the sample stage moved to the measurement position with your new sample. The Xray eye will automatically move in and the shutter will open. You may already see the sample in the omny xeye interface running on the windows computer.
If you see your sample already at the approximately correct height, you can skip steps 1 to 3. Otherwise adjust the height:
1. `flomni.rt_feedback_disable()` disable the closed loop operation to allow movement of coarse stages
1. `flomni.feedback_disable()` disable the closed loop operation to allow movement of coarse stages
1. `umvr(dev.fsamy, 0.01)`, attention: unit <mm>, move the sample stage relative up (positive) or down (negative) until the sample is approximately vertically centered in xray eye screen
1. `flomni.xrayeye_update_frame()` will update the current image on the xray eye screen
1. `flomni.xrayeye_alignment_start()` start the coarse alignment of the sample by measuring (clicking in the X-ray eye software) the sample position at 0, 45, 90, 135, 180 degrees. Then use the matlab routine `SPEC_ptycho_align.m` to fit this data.
1. `flomni.read_alignment_offset()` read the generated alignment data.
1. `flomni.xrayeye_alignment_start()` start the coarse alignment of the sample by measuring (clicking in the X-ray eye software) the sample position at 0, 45, 90, 135, 180 degrees. The GUI will present a fit of this data, which is automatically loaded to BEC for aligning the sample.
#### Fine alignment
After the xrayeyealign, a fine alignment needs to be performed using ptychography.
_To bypass the fine alignment: `feye_out`_
_To bypass the fine alignment: `flomni.feye_out`_
1. `flomni.tomo_parameters()` Adjust the ptychographic scan parameters for performing an alignment scan. Typically FOVX = FOVX(Xrayeye)+20 mu, shell step = beamsize/2.5, number of projections and tomo mode are ignored in the alignment scans.
@@ -71,13 +70,13 @@ _To bypass the fine alignment: `feye_out`_
Now that the sample is aligned, the tomographic measurement can be performed.
1. `flomni.tomo_parameters()` adjust the scan parameters for the tomographic scan. This includes the parameters for ptychographic scans of projections plus the strategy for angular sampling. The vertical shift adjusts the field of view, up (positive) or down (negative). After adjusting the numbers, type again `flomni.tomo_parameters()` and verify that they are correct.
1. `flomni.tomo_scan_projection(angle)` perform a ptychographic scan at the rotation angle <angle>. Launch the tomographic measurement by `flomni.tomo_scan()`.
1. Before changing sample, verify that all subtomograms were completely acquired using the `tomo_recons matlab` script.
1. Before changing sample, verify that all subtomograms were completely acquired using the tomo_reconstruction matlab script.
#### If something went wrong…
A __single projection__ is to be repeated use
`flomni.tomo_scan_projection(<angle>)`. The target angle of scans can be found in the second column of the file in
`~/Data10/specES1/dat-files/omni_scannumbers.txt`
`~/data/raw/logs/tomography_scannumbers.txt`
To continue an __interrupted tomography scan__:

View File

@@ -26,7 +26,7 @@ The effective position of the axis of rotation shifts with sample thickness or m
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: `fshclose()`
1. close the shutter: `dev.omnyfsh.fshclose()`
#### X-ray eye alignment

View File

@@ -287,19 +287,20 @@ def test_ddg1_stage(mock_ddg1: DDG1):
mock_ddg1.stage()
shutter_width = mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger
total_exposure = 2 * mock_ddg1._shutter_to_open_delay + exp_time * frames_per_trigger + 3e-6
assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled
assert np.isclose(mock_ddg1.burst_delay.get(), 0)
assert np.isclose(mock_ddg1.burst_period.get(), shutter_width)
assert np.isclose(mock_ddg1.burst_period.get(), total_exposure)
# Trigger DDG2 through EXT/EN
assert np.isclose(mock_ddg1.ab.delay.get(), 2e-3)
assert np.isclose(mock_ddg1.ab.width.get(), 1e-6)
assert np.isclose(mock_ddg1.ab.width.get(), shutter_width)
# Shutter channel cd
assert np.isclose(mock_ddg1.cd.delay.get(), 0)
assert np.isclose(mock_ddg1.cd.width.get(), shutter_width)
# MCS channel ef or gate
assert np.isclose(mock_ddg1.ef.delay.get(), 0)
assert np.isclose(mock_ddg1.ef.delay.get(), 1e-6)
assert np.isclose(mock_ddg1.ef.width.get(), 1e-6)
assert mock_ddg1.staged == ophyd.Staged.yes