diff --git a/bec_plugins/__init__.py b/bec_plugins/__init__.py index e69de29..c8ba5d1 100644 --- a/bec_plugins/__init__.py +++ b/bec_plugins/__init__.py @@ -0,0 +1 @@ +from .bec_client import * diff --git a/bec_plugins/bec_client/__init__.py b/bec_plugins/bec_client/__init__.py new file mode 100644 index 0000000..ba24808 --- /dev/null +++ b/bec_plugins/bec_client/__init__.py @@ -0,0 +1 @@ +from .plugins import * diff --git a/bec_plugins/plugins/__init__.py b/bec_plugins/bec_client/hli/__init__.py similarity index 100% rename from bec_plugins/plugins/__init__.py rename to bec_plugins/bec_client/hli/__init__.py diff --git a/bec_plugins/plugins/bec_client/hli/spec_hli.py b/bec_plugins/bec_client/hli/spec_hli.py similarity index 100% rename from bec_plugins/plugins/bec_client/hli/spec_hli.py rename to bec_plugins/bec_client/hli/spec_hli.py diff --git a/bec_plugins/plugins/bec_client/plugins/LamNI/LamNI_logo.png b/bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png similarity index 100% rename from bec_plugins/plugins/bec_client/plugins/LamNI/LamNI_logo.png rename to bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png diff --git a/bec_plugins/bec_client/plugins/LamNI/__init__.py b/bec_plugins/bec_client/plugins/LamNI/__init__.py new file mode 100644 index 0000000..e651823 --- /dev/null +++ b/bec_plugins/bec_client/plugins/LamNI/__init__.py @@ -0,0 +1,2 @@ +from .load_additional_correction import lamni_read_additional_correction +from .x_ray_eye_align import LamNI, XrayEyeAlign, MagLamNI, DataDrivenLamNI diff --git a/bec_plugins/plugins/bec_client/plugins/LamNI/bl_show_all.mac b/bec_plugins/bec_client/plugins/LamNI/bl_show_all.mac similarity index 100% rename from bec_plugins/plugins/bec_client/plugins/LamNI/bl_show_all.mac rename to bec_plugins/bec_client/plugins/LamNI/bl_show_all.mac diff --git a/bec_plugins/plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py b/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py similarity index 81% rename from bec_plugins/plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py rename to bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py index 19a9059..ccf6d95 100644 --- a/bec_plugins/plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py +++ b/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py @@ -1,9 +1,16 @@ +import builtins import time + +from rich import box from rich.console import Console from rich.table import Table -from rich import box -from bec_client.plugins.cSAXS import fshclose +from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose + +# import builtins to avoid linter errors +dev = builtins.__dict__.get("dev") +umv = builtins.__dict__.get("umv") +bec = builtins.__dict__.get("bec") class LamNIOpticsMixin: @@ -14,18 +21,22 @@ class LamNIOpticsMixin: raise ValueError(f"Device {device} has no user parameter definition for {var}.") return param.get(var) - def leyey_out(self): + def leye_out(self): self.loptics_in() fshclose() leyey_out = self._get_user_param_safe("leyey", "out") umv(dev.leyey, leyey_out) epics_put("XOMNYI-XEYE-ACQ:0", 2) - umv(dev.dttrz, 5830, dev.fttrz, 3338) + # move rotation stage to zero to avoid problems with wires + umv(dev.lsamrot, 0) + umv(dev.dttrz, 5854, dev.fttrz, 2395) def leye_in(self): bec.queue.next_dataset_number += 1 - umv(dev.dttrz, 5830 + 600, dev.fttrz, 3338 + 600) + # move rotation stage to zero to avoid problems with wires + umv(dev.lsamrot, 0) + 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": @@ -50,12 +61,12 @@ class LamNIOpticsMixin: This will disable rt feedback, move the FZP and re-enabled the feedback. """ if "rtx" in dev and dev.rtx.enabled: - dev.rtx.feedback_disable() + dev.rtx.controller.feedback_disable() self._lfzp_in() if "rtx" in dev and dev.rtx.enabled: - dev.rtx.feedback_enable_with_reset() + dev.rtx.controller.feedback_enable_with_reset() def loptics_in(self): """ @@ -67,7 +78,7 @@ class LamNIOpticsMixin: def loptics_out(self): """Move out the lamni optics""" if "rtx" in dev and dev.rtx.enabled: - dev.rtx.feedback_disable() + dev.rtx.controller.feedback_disable() # self.lcs_out() self.losa_out() @@ -77,7 +88,7 @@ class LamNIOpticsMixin: if "rtx" in dev and dev.rtx.enabled: time.sleep(1) - dev.rtx.feedback_enable_with_reset() + dev.rtx.controller.feedback_enable_with_reset() def lcs_in(self): # umv lcsx -1.852 lcsy -0.095 @@ -114,15 +125,7 @@ class LamNIOpticsMixin: distance = -loptz_val + 85.6 + 52 print(f"The sample is in a distance of {distance:.1f} mm from the FZP.") - diameters = [] - diameters[0] = 80e-6 - diameters[1] = 100e-6 - diameters[2] = 120e-6 - diameters[3] = 150e-6 - diameters[4] = 170e-6 - diameters[5] = 200e-6 - diameters[6] = 220e-6 - diameters[7] = 250e-6 + diameters = [80e-6, 100e-6, 120e-6, 150e-6, 170e-6, 200e-6, 220e-6, 250e-6] mokev_val = dev.mokev.read()["mokev"]["value"] console = Console() @@ -142,7 +145,11 @@ class LamNIOpticsMixin: beam_size = ( -diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance) * 1e6 ) - table.add_row(f"{diameter}", f"{focal_distance:.2f} mm", f"{beam_size:.2f} microns") + table.add_row( + f"{diameter*1e6:.2f} microns", + f"{focal_distance:.2f} mm", + f"{beam_size:.2f} microns", + ) console.print(table) diff --git a/bec_plugins/plugins/bec_client/plugins/LamNI/load_additional_correction.py b/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py similarity index 100% rename from bec_plugins/plugins/bec_client/plugins/LamNI/load_additional_correction.py rename to bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py diff --git a/bec_plugins/plugins/bec_client/plugins/LamNI/x_ray_eye_align.py b/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py similarity index 61% rename from bec_plugins/plugins/bec_client/plugins/LamNI/x_ray_eye_align.py rename to bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py index b4010cb..6e1d2bb 100644 --- a/bec_plugins/plugins/bec_client/plugins/LamNI/x_ray_eye_align.py +++ b/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py @@ -1,6 +1,5 @@ import builtins import datetime -import math import os import subprocess import threading @@ -8,10 +7,11 @@ import time from collections import defaultdict from pathlib import Path +import h5py import numpy as np -from bec_utils import bec_logger -from bec_utils.logbook_connector import LogbookMessage -from bec_utils.pdf_writer import PDFWriter +from bec_lib.alarm_handler import AlarmBase +from bec_lib.core import bec_logger +from bec_lib.core.pdf_writer import PDFWriter from typeguard import typechecked from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose, fshopen @@ -19,6 +19,7 @@ from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose, fshopen from .lamni_optics_mixin import LamNIOpticsMixin logger = bec_logger.logger +bec = builtins.__dict__.get("bec") class XrayEyeAlign: @@ -33,6 +34,58 @@ class XrayEyeAlign: self.scans = client.scans self.xeye = self.device_manager.devices.xeye 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 = [] + + 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") + + @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]) + + def _reset_init_values(self): self.shift_xy = [0, 0] self._xray_fov_xy = [0, 0] @@ -93,6 +146,11 @@ class XrayEyeAlign: epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg) def align(self): + # reset shift xy and fov params + self._reset_init_values() + self.reset_correction() + self.reset_correction_2() + # this makes sure we are in a defined state self._disable_rt_feedback() @@ -156,9 +214,9 @@ class XrayEyeAlign: elif ( k == 1 ): # received sample center value at samroy 0 ie the final base shift values - print( - f"Base shift values from movement are x {self.shift_xy[0]}, y {self.shift_xy[1]}" - ) + msg = f"Base shift values from movement are x {self.shift_xy[0]}, y {self.shift_xy[1]}" + print(msg) + logger.info(msg) self.shift_xy[0] += ( self.alignment_values[0][0] - self.alignment_values[1][0] ) * 1000 @@ -170,7 +228,9 @@ class XrayEyeAlign: ) self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + self.shift_xy[0] / 1000, + self.shift_xy[1] / 1000, + self.get_tomo_angle(), ).wait() self.send_message("please wait ...") @@ -179,7 +239,9 @@ class XrayEyeAlign: time.sleep(1) self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, self.shift_xy[1] / 1000, self.get_tomo_angle() + 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()) @@ -196,12 +258,16 @@ class XrayEyeAlign: 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() + 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() + 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()) @@ -228,7 +294,9 @@ class XrayEyeAlign: 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() + 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]}" @@ -256,7 +324,7 @@ class XrayEyeAlign: self.tomo_rotate(0) print( - "\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running lamni.align.read_alignment_parameters()\n" + "\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) @@ -274,7 +342,7 @@ class XrayEyeAlign: ) alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n") - def read_alignment_parameters(self, dir_path=os.path.expanduser("~/Data10/specES1/internal/")): + def read_xray_eye_correction(self, dir_path=os.path.expanduser("~/Data10/specES1/internal/")): tomo_fit_xray_eye = np.zeros((2, 3)) with open(os.path.join(dir_path, "ptychotomoalign_Ax.txt"), "r") as file: tomo_fit_xray_eye[0][0] = file.readline() @@ -308,20 +376,150 @@ class XrayEyeAlign: f"Y Offset {tomo_fit_xray_eye[1][2]}" ) + def lamni_compute_additional_correction_xeye_mu(self, 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) + + def compute_additional_correction(self, angle): + if not self.corr_pos_x: + print("Not applying any additional correction. No data available.\n") + return (0, 0) + + # find index of closest angle + for j, _ in enumerate(self.corr_pos_x): + newangledelta = np.fabs(self.corr_angle[j] - angle) + if j == 0: + angledelta = newangledelta + additional_correction_shift_x = self.corr_pos_x[j] + additional_correction_shift_y = self.corr_pos_y[j] + continue + + if newangledelta < angledelta: + additional_correction_shift_x = self.corr_pos_x[j] + additional_correction_shift_y = self.corr_pos_y[j] + angledelta = newangledelta + + if additional_correction_shift_x == 0 and angle < self.corr_angle[0]: + additional_correction_shift_x = self.corr_pos_x[0] + additional_correction_shift_y = self.corr_pos_y[0] + + if additional_correction_shift_x == 0 and angle > self.corr_angle[-1]: + additional_correction_shift_x = self.corr_pos_x[-1] + additional_correction_shift_y = self.corr_pos_y[-1] + print( + f"Additional correction shifts: {additional_correction_shift_x} {additional_correction_shift_y}" + ) + return (additional_correction_shift_x, additional_correction_shift_y) + + def read_additional_correction(self, correction_file: str): + 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)) + self.corr_pos_x = corr_pos_x + self.corr_pos_y = corr_pos_y + self.corr_angle = corr_angle + return + + def compute_additional_correction_2(self, angle): + if not self.corr_pos_x_2: + print("Not applying any additional secondary correction. No data available.\n") + return (0, 0) + + # find index of closest angle + for j, _ in enumerate(self.corr_pos_x_2): + newangledelta = np.fabs(self.corr_angle_2[j] - angle) + if j == 0: + angledelta = newangledelta + additional_correction_shift_x = self.corr_pos_x_2[j] + additional_correction_shift_y = self.corr_pos_y_2[j] + continue + + if newangledelta < angledelta: + additional_correction_shift_x = self.corr_pos_x_2[j] + additional_correction_shift_y = self.corr_pos_y_2[j] + angledelta = newangledelta + + if additional_correction_shift_x == 0 and angle < self.corr_angle_2[0]: + additional_correction_shift_x = self.corr_pos_x_2[0] + additional_correction_shift_y = self.corr_pos_y_2[0] + + if additional_correction_shift_x == 0 and angle > self.corr_angle_2[-1]: + additional_correction_shift_x = self.corr_pos_x_2[-1] + additional_correction_shift_y = self.corr_pos_y_2[-1] + print( + f"Additional correction shifts 2: {additional_correction_shift_x} {additional_correction_shift_y}" + ) + return (additional_correction_shift_x, additional_correction_shift_y) + + def read_additional_correction_2(self, correction_file: str): + 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)) + self.corr_pos_x_2 = corr_pos_x + self.corr_pos_y_2 = corr_pos_y + self.corr_angle_2 = corr_angle + return + class LamNI(LamNIOpticsMixin): def __init__(self, client): super().__init__() self.client = client self.align = XrayEyeAlign(client, self) - self.corr_pos_x = [] - self.corr_pos_y = [] - self.corr_angle = [] + self.check_shutter = True self.check_light_available = True self.check_fofb = True self._check_msgs = [] - self.tomo_id = None + self.tomo_id = -1 + self.special_angles = [] + self.special_angle_repeats = 20 + self.special_angle_tolerance = 20 + self._current_special_angles = [] self._beam_is_okay = True self._stop_beam_check_event = None self.beam_check_thread = None @@ -331,33 +529,38 @@ class LamNI(LamNIOpticsMixin): f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available: {self.check_light_available}" ) - def set_beamline_checks_enabled(self, val: bool): + @property + def beamline_checks_enabled(self): + return { + "shutter": self.check_shutter, + "fofb": self.check_fofb, + "light available": self.check_light_available, + } + + @beamline_checks_enabled.setter + def beamline_checks_enabled(self, val: bool): self.check_shutter = val self.check_light_available = val self.check_fofb = val self.get_beamline_checks_enabled() - @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 + def set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5): + """Set the special angles for a tomo - @tomo_fovx_offset.setter - def tomo_fovx_offset(self, val: float): - self.client.set_global_var("tomo_fov_offset", val) + Args: + angles (list): List of special angles + repeats (int, optional): Number of repeats at a special angle. Defaults to 20. + tolerance (float, optional): Number of repeats at a special angle. Defaults to 0.5. - @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 + """ + self.special_angles = angles + self.special_angle_repeats = repeats + self.special_angle_tolerance = tolerance - @tomo_fovy_offset.setter - def tomo_fovy_offset(self, val: float): - self.client.set_global_var("tomo_fov_offset", val) + def remove_special_angles(self): + """Remove the special angles and set the number of repeats to 1""" + self.special_angles = [] + self.special_angle_repeats = 1 @property def tomo_shellstep(self): @@ -423,6 +626,9 @@ class LamNI(LamNIOpticsMixin): @lamni_piezo_range_x.setter def lamni_piezo_range_x(self, val: float): + if dev.rtx.user_parameter and dev.rtx.user_parameter.get("large_range_scan", True): + self.client.set_global_var("lamni_piezo_range_x", val) + return if val > 80: raise ValueError("Piezo range cannot be larger than 80 um.") self.client.set_global_var("lamni_piezo_range_x", val) @@ -436,10 +642,24 @@ class LamNI(LamNIOpticsMixin): @lamni_piezo_range_y.setter def lamni_piezo_range_y(self, val: float): + if dev.rtx.user_parameter and dev.rtx.user_parameter.get("large_range_scan", True): + self.client.set_global_var("lamni_piezo_range_y", val) + return if val > 80: raise ValueError("Piezo range cannot be larger than 80 um.") self.client.set_global_var("lamni_piezo_range_y", val) + @property + def corridor_size(self): + val = self.client.get_global_var("corridor_size") + if val is None: + val = -1 + return val + + @corridor_size.setter + def corridor_size(self, val: float): + self.client.set_global_var("corridor_size", val) + @property def lamni_stitch_x(self): val = self.client.get_global_var("lamni_stitch_x") @@ -497,11 +717,48 @@ class LamNI(LamNIOpticsMixin): def tomo_stitch_overlap(self, val: float): self.client.set_global_var("tomo_stitch_overlap", val) + @property + def sample_name(self): + val = self.client.get_global_var("sample_name") + if val is None: + return "bec_test_sample" + return val + + @sample_name.setter + @typechecked + def sample_name(self, val: str): + self.client.set_global_var("sample_name", val) + + 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: + tags.append("BEC") + else: + tags = ["BEC"] + msg = bec.logbook.LogbookMessage() + msg.add_text(content).add_tag(tags) + self.client.logbook.send_logbook_message(msg) + except Exception: + logger.warning("Failed to write to scilog.") + def tomo_scan_projection(self, angle: float): scans = builtins.__dict__.get("scans") - bec = builtins.__dict__.get("bec") - additional_correction = self.compute_additional_correction(angle) - correction_xeye_mu = self.lamni_compute_additional_correction_xeye_mu(angle) + + 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) self._current_scan_list = [] @@ -511,106 +768,42 @@ class LamNI(LamNIOpticsMixin): self._current_scan_list.append(bec.queue.next_scan_number) logger.info( f"scans.lamni_fermat_scan(fov_size=[{self.lamni_piezo_range_x},{self.lamni_piezo_range_y}], step={self.tomo_shellstep}, stitch_x={0}, stitch_y={0}, stitch_overlap={1}," - f"center_x={self.tomo_fovx_offset}, center_y={self.tomo_fovy_offset}, " - f"shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]}, " - f"shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]}, " + f"center_x={self.align.tomo_fovx_offset}, center_y={self.align.tomo_fovy_offset}, " + f"shift_x={self.manual_shift_x+correction_xeye_mu[0]-additional_correction[0]-additional_correction_2[0]}, " + f"shift_y={self.manual_shift_y+correction_xeye_mu[1]-additional_correction[1]-additional_correction_2[1]}, " f"fov_circular={self.tomo_circfov}, angle={angle}, scan_type='fly')" ) + log_message = f"{str(datetime.datetime.now())}: LamNI scan projection at angle {angle}, scan 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]) + 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], step=self.tomo_shellstep, stitch_x=stitch_x, stitch_y=stitch_y, stitch_overlap=self.tomo_stitch_overlap, - center_x=self.tomo_fovx_offset, - center_y=self.tomo_fovy_offset, + center_x=self.align.tomo_fovx_offset, + center_y=self.align.tomo_fovy_offset, shift_x=( - self.manual_shift_x + correction_xeye_mu[0] - additional_correction[0] + self.manual_shift_x + + correction_xeye_mu[0] + - additional_correction[0] + - additional_correction_2[0] ), shift_y=( - self.manual_shift_y + correction_xeye_mu[1] - additional_correction[1] + self.manual_shift_y + + correction_xeye_mu[1] + - additional_correction[1] + - additional_correction_2[1] ), fov_circular=self.tomo_circfov, angle=angle, scan_type="fly", exp_time=self.tomo_countingtime, + optim_trajectory_corridor=corridor_size, ) - def lamni_compute_additional_correction_xeye_mu(self, 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] * math.sin(math.radians(angle) + tomo_fit_xray_eye[0][1]) - + tomo_fit_xray_eye[0][2] - ) / 1000 - correction_y = ( - tomo_fit_xray_eye[1][0] * math.sin(math.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) - - def compute_additional_correction(self, angle): - if not self.corr_pos_x: - print("Not applying any additional correction. No data available.\n") - return (0, 0) - - # find index of closest angle - for j, _ in enumerate(self.corr_pos_x): - newangledelta = math.fabs(self.corr_angle[j] - angle) - if j == 0: - angledelta = newangledelta - additional_correction_shift_x = self.corr_pos_x[j] - additional_correction_shift_y = self.corr_pos_y[j] - continue - - if newangledelta < angledelta: - additional_correction_shift_x = self.corr_pos_x[j] - additional_correction_shift_y = self.corr_pos_y[j] - angledelta = newangledelta - - if additional_correction_shift_x == 0 and angle < self.corr_angle[0]: - additional_correction_shift_x = self.corr_pos_x[0] - additional_correction_shift_y = self.corr_pos_y[0] - - if additional_correction_shift_x == 0 and angle > self.corr_angle[-1]: - additional_correction_shift_x = self.corr_pos_x[-1] - additional_correction_shift_y = self.corr_pos_y[-1] - logger.info( - f"Additional correction shifts: {additional_correction_shift_x} {additional_correction_shift_y}" - ) - return (additional_correction_shift_x, additional_correction_shift_y) - - def lamni_read_additional_correction(self, correction_file: str): - - 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)) - self.corr_pos_x = corr_pos_x - self.corr_pos_y = corr_pos_y - self.corr_angle = corr_angle - return - def _run_beamline_checks(self): msgs = [] dev = builtins.__dict__.get("dev") @@ -622,7 +815,10 @@ class LamNI(LamNIOpticsMixin): msgs.append("Check beam failed: Shutter is closed.") if self.check_light_available: machine_status = dev.sls_machine_status.read(cached=True) - if machine_status["value"] not in ["Light Available", "Light-Available"]: + if machine_status["value"] not in [ + "Light Available", + "Light-Available", + ]: self._beam_is_okay = False msgs.append("Check beam failed: Light not available.") if self.check_fofb: @@ -661,7 +857,7 @@ class LamNI(LamNIOpticsMixin): def _wait_for_beamline_checks(self): self._print_beamline_checks() try: - msg = LogbookMessage(self.client.logbook) + msg = bec.logbook.LogbookMessage() msg.add_text( f"
Beamline checks failed at {str(datetime.datetime.now())}: {''.join(self._check_msgs)}
" ).add_tag(["BEC", "beam_check"]) @@ -678,7 +874,7 @@ class LamNI(LamNIOpticsMixin): time.sleep(1) try: - msg = LogbookMessage(self.client.logbook) + msg = bec.logbook.LogbookMessage() msg.add_text( f"Operation resumed at {str(datetime.datetime.now())}.
" ).add_tag(["BEC", "beam_check"]) @@ -687,7 +883,14 @@ class LamNI(LamNIOpticsMixin): logger.warning("Failed to send update to SciLog.") def add_sample_database( - self, samplename, date, eaccount, scan_number, setup, sample_additional_info, user + self, + samplename, + date, + eaccount, + scan_number, + setup, + sample_additional_info, + user, ): """Add a sample to the omny sample database. This also retrieves the tomo id.""" subprocess.run( @@ -698,10 +901,31 @@ class LamNI(LamNIOpticsMixin): tomo_number = int(tomo_number_file.read()) return tomo_number + def _at_each_angle(self, angle: float) -> None: + self.tomo_scan_projection(angle) + self.tomo_reconstruct() + + ### XMCD ### + # 2 projections, 1 for each polarization state + # cp() + # self.tomo_scan_projection(angle) + # self.tomo_reconstruct() + # cm() + # self.tomo_scan_projection(angle) + # self.tomo_reconstruct() + def sub_tomo_scan(self, subtomo_number, start_angle=None): """start a subtomo""" dev = builtins.__dict__.get("dev") bec = builtins.__dict__.get("bec") + if self.tomo_id > 0: + tags = ["BEC_subtomo", self.sample_name, f"tomo_id_{self.tomo_id}"] + else: + tags = ["BEC_subtomo", self.sample_name] + self.write_to_scilog( + f"Starting subtomo: {subtomo_number}. First scan number: {bec.queue.next_scan_number}.", + tags, + ) if start_angle is None: if subtomo_number == 1: @@ -731,44 +955,73 @@ class LamNI(LamNIOpticsMixin): endpoint=True, ): successful = False + error_caught = False if 0 <= angle < 360.05: print(f"Starting LamNI scan for angle {angle}") while not successful: self._start_beam_check() - self.tomo_scan_projection(angle) + if not self.special_angles: + self._current_special_angles = [] + if self._current_special_angles: + next_special_angle = self._current_special_angles[0] + if np.isclose(angle, next_special_angle, atol=0.5): + self._current_special_angles.pop(0) + num_repeats = self.special_angle_repeats + else: + num_repeats = 1 + try: + start_scan_number = bec.queue.next_scan_number + for i in range(num_repeats): + self._at_each_angle(angle) + error_caught = False + except AlarmBase as exc: + if exc.alarm_type == "TimeoutError": + bec.queue.request_queue_reset() + time.sleep(2) + error_caught = True + else: + raise exc - if self._was_beam_okay(): + if self._was_beam_okay() and not error_caught: successful = True else: self._wait_for_beamline_checks() - self.tomo_reconstruct() - tomo_id = 0 - with open( - os.path.expanduser("~/Data10/specES1/dat-files/tomography_scannumbers.txt"), - "a+", - ) as out_file: - # pylint: disable=undefined-variable - out_file.write( - f"{bec.queue.next_scan_number-1} {angle} {dev.lsamrot.read()['lsamrot']['value']} {self.tomo_id} {subtomo_number} {0} {'lamni'}\n" - ) + 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) + + 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" + ) + with open(tomo_scan_numbers_file, "a+") as out_file: + # pylint: disable=undefined-variable + out_file.write( + f"{scan_number} {angle} {dev.lsamrot.read()['lsamrot']['value']:.3f} {self.tomo_id} {subtomo_number} {0} {'lamni'}\n" + ) def tomo_scan(self, subtomo_start=1, start_angle=None): """start a tomo scan""" bec = builtins.__dict__.get("bec") + scans = builtins.__dict__.get("scans") + self._current_special_angles = self.special_angles.copy() + if subtomo_start == 1 and start_angle is None: # pylint: disable=undefined-variable self.tomo_id = self.add_sample_database( - "bec_test_sample", + self.sample_name, str(datetime.date.today()), - "e20588", + bec.active_account.decode(), bec.queue.next_scan_number, "lamni", "test additional info", "BEC", ) - for ii in range(subtomo_start, 9): - self.sub_tomo_scan(ii, start_angle=start_angle) - start_angle = None + self.write_pdf_report() + with scans.dataset_id_on_hold: + for ii in range(subtomo_start, 9): + self.sub_tomo_scan(ii, start_angle=start_angle) + start_angle = None def tomo_parameters(self): """print and update the tomo parameters""" @@ -785,12 +1038,13 @@ class LamNI(LamNIOpticsMixin): print( "For information, fov offset is rotating and finding the ROI, manual shift moves rotation center" ) - print(f" _tomo_fovx_offset")).add_tag(
- ["BEC", "tomo_parameters", f"dataset_id_{dataset_id}", "LamNI"]
+ [
+ "BEC",
+ "tomo_parameters",
+ f"dataset_id_{dataset_id}",
+ "LamNI",
+ self.sample_name,
+ ]
)
self.client.logbook.send_logbook_message(msg)
+
+
+class MagLamNI(LamNI):
+ def sub_tomo_scan(self, subtomo_number, start_angle=None):
+ super().sub_tomo_scan(subtomo_number, start_angle)
+ # self.rotate_slowly(0)
+
+ def rotate_slowly(self, angle, step_size=20):
+ current_angle = dev.lsamrot.read(cached=True)["value"]
+ steps = int(np.ceil(np.abs(current_angle - angle) / step_size)) + 1
+ for target_angle in np.linspace(current_angle, angle, steps, endpoint=True):
+ umv(dev.lsamrot, target_angle)
+ scans.lamni_move_to_scan_center(
+ self.align.tomo_fovx_offset, self.align.tomo_fovy_offset, target_angle
+ )
+
+ def _at_each_angle(self, angle: float) -> None:
+ if "lamni_at_each_angle" in builtins.__dict__:
+ lamni_at_each_angle(self, angle)
+ return
+
+ self.tomo_scan_projection(angle)
+ self.tomo_reconstruct()
+
+ # # cm()
+ # # umv(dev.ppth,15.1762) #11.567 keV
+ # for ii in range(2):
+ # self.tomo_scan_projection(angle)
+ # self.tomo_reconstruct()
+ # # cp()
+ # # umv(dev.ppth,15.1827) #11.567 keV
+ # for ii in range(2):
+ # self.tomo_scan_projection(angle)
+ # self.tomo_reconstruct()
+
+
+class DataDrivenLamNI(LamNI):
+ def __init__(self, client):
+ super().__init__(client)
+ self.tomo_data = {}
+
+ def tomo_scan(
+ self,
+ subtomo_start=1,
+ start_index=None,
+ fname="~/Data10/data_driven_config/datadriven_params.h5",
+ ):
+ """start a tomo scan"""
+ bec = builtins.__dict__.get("bec")
+ scans = builtins.__dict__.get("scans")
+
+ fname = os.path.expanduser(fname)
+
+ if not os.path.exists(fname):
+ raise FileNotFoundError(f"Could not find datadriven params file in {fname}.")
+ content = f"Loading tomo parameters from {fname}."
+ logger.warning(content)
+ tags = ["Data_driven_file", "BEC"]
+ msg = bec.logbook.LogbookMessage()
+ msg.add_text(content).add_tag(tags)
+ self.client.logbook.send_logbook_message(msg)
+ self._update_tomo_data_from_file(fname)
+
+ self._current_special_angles = self.special_angles.copy()
+
+ if subtomo_start == 1 and start_index is None:
+ # pylint: disable=undefined-variable
+ self.tomo_id = self.add_sample_database(
+ self.sample_name,
+ str(datetime.date.today()),
+ bec.active_account.decode(),
+ bec.queue.next_scan_number,
+ "lamni",
+ "test additional info",
+ "BEC",
+ )
+ self.write_pdf_report()
+ with scans.dataset_id_on_hold:
+ self.sub_tomo_data_driven(start_index)
+
+ def sub_tomo_scan(self):
+ raise NotImplementedError(
+ "Cannot run sub_tomo_scan with data-driven LamNI. Please use lamni.tomo_scan(subtomo_start=