From 26fe056c4a5c8ca8c95b8ca6f540487ef7575e0d Mon Sep 17 00:00:00 2001 From: Klaus Wakonig Date: Fri, 7 Jul 2023 14:39:22 +0200 Subject: [PATCH] testing deployment --- bec_plugins/__init__.py | 1 + bec_plugins/bec_client/__init__.py | 1 + .../{plugins => bec_client/hli}/__init__.py | 0 .../{plugins => }/bec_client/hli/spec_hli.py | 0 .../bec_client/plugins/LamNI/LamNI_logo.png | Bin .../bec_client/plugins/LamNI/__init__.py | 2 + .../bec_client/plugins/LamNI/bl_show_all.mac | 0 .../plugins/LamNI/lamni_optics_mixin.py | 45 +- .../LamNI/load_additional_correction.py | 0 .../plugins/LamNI/x_ray_eye_align.py | 761 ++++++++++++++---- bec_plugins/bec_client/plugins/__init__.py | 3 + .../bec_client/plugins/cSAXS/__init__.py | 0 .../bec_client/plugins/cSAXS/beamline_info.py | 108 +++ .../plugins/cSAXS/cSAXS_beamline.py | 0 .../startup/__init__.py} | 0 .../bec_client/startup/post_startup.py | 61 ++ bec_plugins/bec_client/startup/pre_startup.py | 25 + .../bec_client/plugins/LamNI/__init__.py | 2 - .../__init__.py} | 0 .../scan_plugins/LamNIFermatScan.py | 0 .../{plugins => }/scibec/lamni_config.py | 0 .../scibec/test_config_cSAXS.yaml | 0 deployment/autodeploy_version | 11 + deployment/deploy.sh | 24 + 24 files changed, 865 insertions(+), 179 deletions(-) create mode 100644 bec_plugins/bec_client/__init__.py rename bec_plugins/{plugins => bec_client/hli}/__init__.py (100%) rename bec_plugins/{plugins => }/bec_client/hli/spec_hli.py (100%) rename bec_plugins/{plugins => }/bec_client/plugins/LamNI/LamNI_logo.png (100%) create mode 100644 bec_plugins/bec_client/plugins/LamNI/__init__.py rename bec_plugins/{plugins => }/bec_client/plugins/LamNI/bl_show_all.mac (100%) rename bec_plugins/{plugins => }/bec_client/plugins/LamNI/lamni_optics_mixin.py (81%) rename bec_plugins/{plugins => }/bec_client/plugins/LamNI/load_additional_correction.py (100%) rename bec_plugins/{plugins => }/bec_client/plugins/LamNI/x_ray_eye_align.py (61%) create mode 100644 bec_plugins/bec_client/plugins/__init__.py rename bec_plugins/{plugins => }/bec_client/plugins/cSAXS/__init__.py (100%) create mode 100644 bec_plugins/bec_client/plugins/cSAXS/beamline_info.py rename bec_plugins/{plugins => }/bec_client/plugins/cSAXS/cSAXS_beamline.py (100%) rename bec_plugins/{plugins/bec_client/startup/post-startup.py => bec_client/startup/__init__.py} (100%) create mode 100644 bec_plugins/bec_client/startup/post_startup.py create mode 100644 bec_plugins/bec_client/startup/pre_startup.py delete mode 100644 bec_plugins/plugins/bec_client/plugins/LamNI/__init__.py rename bec_plugins/{plugins/bec_client/startup/pre-startup.py => scan_server/__init__.py} (100%) rename bec_plugins/{plugins => }/scan_server/scan_plugins/LamNIFermatScan.py (100%) rename bec_plugins/{plugins => }/scibec/lamni_config.py (100%) rename bec_plugins/{plugins => }/scibec/test_config_cSAXS.yaml (100%) create mode 100644 deployment/autodeploy_version create mode 100755 deployment/deploy.sh 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 = {self.tomo_fovx_offset}") - print(f" _tomo_fovy_offset = {self.tomo_fovy_offset}") + print(f" _tomo_fovx_offset = {self.align.tomo_fovx_offset}") + print(f" _tomo_fovy_offset = {self.align.tomo_fovy_offset}") print(f" _manual_shift_x = {self.manual_shift_x}") print(f" _manual_shift_y = {self.manual_shift_y}") print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") - print(f"Resulting in number of projections: {360/self.tomo_angle_stepsize*8}\n") + print(f"Resulting in number of projections: {360/self.tomo_angle_stepsize*8}") + print(f"Sample name: {self.sample_name}\n") user_input = input("Are these parameters correctly set for your scan? ") if user_input == "y": @@ -817,6 +1071,7 @@ class LamNI(LamNIOpticsMixin): print(f"The angular step will be {360/tomo_numberofprojections}") self.tomo_angle_stepsize = 360 / tomo_numberofprojections * 8 print(f"The angular step in a subtomogram it will be {self.tomo_angle_stepsize}") + self.sample_name = self._get_val("sample name", self.sample_name, str) @staticmethod def _get_val(msg: str, default_value, data_type): @@ -856,9 +1111,8 @@ class LamNI(LamNIOpticsMixin): piezo_range = f"{self.lamni_piezo_range_x:.2f}/{self.lamni_piezo_range_y:.2f}" stitching = f"{self.lamni_stitch_x:.2f}/{self.lamni_stitch_y:.2f}" dataset_id = str(self.client.queue.next_dataset_number) - # pylint: disable=undefined-variable - content = ( - f"{'Sample Name:':<{padding}}{'test':>{padding}}\n", + content = [ + f"{'Sample Name:':<{padding}}{self.sample_name:>{padding}}\n", f"{'Measurement ID:':<{padding}}{str(self.tomo_id):>{padding}}\n", f"{'Dataset ID:':<{padding}}{dataset_id:>{padding}}\n", f"{'Sample Info:':<{padding}}{'Sample Info':>{padding}}\n", @@ -866,23 +1120,214 @@ class LamNI(LamNIOpticsMixin): f"{'Number of projections:':<{padding}}{int(360 / self.tomo_angle_stepsize * 8):>{padding}}\n", f"{'First scan number:':<{padding}}{self.client.queue.next_scan_number:>{padding}}\n", f"{'Last scan number approx.:':<{padding}}{self.client.queue.next_scan_number + int(360 / self.tomo_angle_stepsize * 8) + 10:>{padding}}\n", - f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['mokev']['value']:>{padding}.4f}\n", + f"{'Current photon energy:':<{padding}}{dev.mokev.read(cached=True)['value']:>{padding}.4f}\n", f"{'Exposure time:':<{padding}}{self.tomo_countingtime:>{padding}.2f}\n", f"{'Fermat spiral step size:':<{padding}}{self.tomo_shellstep:>{padding}.2f}\n", - f"{'Piezo range (FOV sample plane):':<{padding}}{piezo_range>{padding}}\n", + f"{'Piezo range (FOV sample plane):':<{padding}}{piezo_range:>{padding}}\n", f"{'Restriction to circular FOV:':<{padding}}{self.tomo_circfov:>{padding}.2f}\n", f"{'Stitching:':<{padding}}{stitching:>{padding}}\n", f"{'Number of individual sub-tomograms:':<{padding}}{8:>{padding}}\n", f"{'Angular step within sub-tomogram:':<{padding}}{self.tomo_angle_stepsize:>{padding}.2f}\n", - ) - - with PDFWriter(os.path.expanduser("~/Data10/")) as file: + ] + content = "".join(content) + user_target = os.path.expanduser(f"~/Data10/documentation/tomo_scan_ID_{self.tomo_id}.pdf") + with PDFWriter(user_target) as file: file.write(header) file.write(content) - - msg = LogbookMessage(self.client.logbook) - logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lamni_logo.png") + subprocess.run( + "xterm /work/sls/spec/local/XOMNY/bin/upload/upload_last_pon.sh &", + shell=True, + ) + # status = subprocess.run(f"cp /tmp/spec-e20131-specES1.pdf {user_target}", shell=True) + msg = bec.logbook.LogbookMessage() + logo_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "LamNI_logo.png") msg.add_file(logo_path).add_text("".join(content).replace("\n", "

")).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=) instead." + ) + + def _at_each_angle( + self, + angle=None, + stepsize=None, + loptz_pos=None, + manual_shift_x=0, + manual_shift_y=0, + ): + # Do something... + # self.tomo_parameters + self.manual_shift_x = manual_shift_x + self.manual_shift_y = manual_shift_y + self.tomo_shellstep = stepsize # in microns + if loptz_pos is not None: + dev.rtx.controller.feedback_disable() + umv(dev.loptz, loptz_pos) + super()._at_each_angle(angle=angle) + + def sub_tomo_data_driven(self, start_index=None): + # for theta, stepsize, sample_to_focus, probe_diameter, subtomo_id in zip(*self.tomo_data.values()): + + for scan_index, scan_data in enumerate(zip(*self.tomo_data.values())): + if start_index and scan_index < start_index: + continue + ( + angle, + stepsize, + loptz_pos, + propagation_distance, + manual_shift_x, + manual_shift_y, + subtomo_number, + ) = scan_data + bec.metadata.update( + {key: float(val) for key, val in zip(self.tomo_data.keys(), scan_data)} + ) + 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() + 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( + float(angle), + stepsize=float(stepsize), + loptz_pos=float(loptz_pos), + manual_shift_x=float(manual_shift_x), + manual_shift_y=float(manual_shift_y), + ) + 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() and not error_caught: + successful = True + else: + self._wait_for_beamline_checks() + 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 _update_tomo_data_from_file(self, fname: str) -> None: + with h5py.File(fname, "r") as file: + self.tomo_data["theta"] = np.array([*file["theta"]]).flatten() + self.tomo_data["stepsize"] = np.array([*file["stepsize"]]).flatten() + self.tomo_data["loptz"] = np.array([*file["loptz"]]).flatten() + self.tomo_data["propagation_distance"] = np.array( + [*file["relative_propagation_distance"]] + ).flatten() + self.tomo_data["manual_shift_x"] = np.array([*file["manual_shift_x"]]).flatten() + self.tomo_data["manual_shift_y"] = np.array([*file["manual_shift_y"]]).flatten() + self.tomo_data["subtomo_id"] = np.array([*file["subtomo_id"]]).flatten() + + shapes = [] + for data in self.tomo_data.values(): + shapes.append(data.shape) + if len(set(shapes)) > 1: + raise ValueError(f"Tomo data file has entries of inconsistent lengths: {shapes}.") diff --git a/bec_plugins/bec_client/plugins/__init__.py b/bec_plugins/bec_client/plugins/__init__.py new file mode 100644 index 0000000..9558172 --- /dev/null +++ b/bec_plugins/bec_client/plugins/__init__.py @@ -0,0 +1,3 @@ +from .cSAXS import * + +# from .LamNI import * diff --git a/bec_plugins/plugins/bec_client/plugins/cSAXS/__init__.py b/bec_plugins/bec_client/plugins/cSAXS/__init__.py similarity index 100% rename from bec_plugins/plugins/bec_client/plugins/cSAXS/__init__.py rename to bec_plugins/bec_client/plugins/cSAXS/__init__.py diff --git a/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py b/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py new file mode 100644 index 0000000..49e6647 --- /dev/null +++ b/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py @@ -0,0 +1,108 @@ +import builtins + +from rich import box +from rich.table import Table + +from bec_client.beamline_mixin import BeamlineShowInfo + + +class BeamlineInfo(BeamlineShowInfo): + def show(self): + """Display information about the current beamline status""" + console = self._get_console() + + table = Table(title="X12SA Info", box=box.SQUARE) + table.add_column("Key", justify="left") + table.add_column("Value", justify="left") + + info = self._get_beamline_info_messages() + self._add_op_status(table, info) + self._add_id_gap(table, info) + self._add_storage_ring_vac(table, info) + self._add_shutter_status(table, info) + self._add_mokev(table, info) + self._add_fe_status(table, info) + self._add_es1_valve(table, info) + self._add_xbox1_pressure(table, info) + self._add_xbox2_pressure(table, info) + + console.print(table) + + def _add_op_status(self, table, info): + val = self._get_info_val(info, "x12sa_op_status") + if val not in ["attended"]: + return table.add_row("Beamline operation", val, style=self.ALARM_STYLE) + return table.add_row("Beamline operation", val, style=self.DEFAULT_STYLE) + + def _add_shutter_status(self, table, info): + val = self._get_info_val(info, "x12sa_es1_shutter_status") + if val.lower() not in ["open"]: + return table.add_row("Shutter", val, style=self.ALARM_STYLE) + return table.add_row("Shutter", val, style=self.DEFAULT_STYLE) + + def _add_storage_ring_vac(self, table, info): + val = self._get_info_val(info, "x12sa_storage_ring_vac") + if val.lower() not in ["ok"]: + return table.add_row("Storage ring vacuum", val, style=self.ALARM_STYLE) + return table.add_row("Storage ring vacuum", val, style=self.DEFAULT_STYLE) + + def _add_es1_valve(self, table, info): + val = self._get_info_val(info, "x12sa_es1_valve") + if val.lower() not in ["open"]: + return table.add_row("ES1 valve", val, style=self.ALARM_STYLE) + return table.add_row("ES1 valve", val, style=self.DEFAULT_STYLE) + + def _add_xbox1_pressure(self, table, info): + MAX_PRESSURE = 2e-6 + val = info["x12sa_exposure_box1_pressure"]["value"] + if val > MAX_PRESSURE: + return table.add_row( + f"Exposure box 1 pressure (limit for opening the valve: {MAX_PRESSURE:.1e} mbar)", + f"{val:.1e} mbar", + style=self.ALARM_STYLE, + ) + return table.add_row("Exposure box 1 pressure", f"{val:.1e} mbar", style=self.DEFAULT_STYLE) + + def _add_xbox2_pressure(self, table, info): + MAX_PRESSURE = 2e-6 + val = info["x12sa_exposure_box2_pressure"]["value"] + if val > MAX_PRESSURE: + return table.add_row( + f"Exposure box 2 pressure (limit for opening the valve: {MAX_PRESSURE:.1e} mbar)", + f"{val:.1e} mbar", + style=self.ALARM_STYLE, + ) + return table.add_row("Exposure box 2 pressure", f"{val:.1e} mbar", style=self.DEFAULT_STYLE) + + def _add_fe_status(self, table, info): + val = self._get_info_val(info, "x12sa_fe_status") + return table.add_row("Front end shutter", val, style=self.DEFAULT_STYLE) + + def _add_id_gap(self, table, info): + val = info["x12sa_id_gap"]["value"] + if val > 8: + return table.add_row("ID gap", f"{val:.3f} mm", style=self.ALARM_STYLE) + return table.add_row("ID gap", f"{val:.3f} mm", style=self.DEFAULT_STYLE) + + def _add_mokev(self, table, info): + val = info["x12sa_mokev"]["value"] + return table.add_row("Selected energy (mokev)", f"{val:.3f} keV", style=self.DEFAULT_STYLE) + + def _get_beamline_info_messages(self) -> dict: + dev = builtins.__dict__.get("dev") + + def _get_bl_msg(info, device_name): + info[device_name] = dev[device_name].read(cached=True) + + info = {} + _get_bl_msg(info, "x12sa_op_status") + _get_bl_msg(info, "x12sa_storage_ring_vac") + _get_bl_msg(info, "x12sa_es1_shutter_status") + _get_bl_msg(info, "x12sa_id_gap") + _get_bl_msg(info, "x12sa_mokev") + _get_bl_msg(info, "x12sa_fe_status") + _get_bl_msg(info, "x12sa_es1_valve") + _get_bl_msg(info, "x12sa_exposure_box1_pressure") + _get_bl_msg(info, "x12sa_exposure_box2_pressure") + + return info diff --git a/bec_plugins/plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py b/bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py similarity index 100% rename from bec_plugins/plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py rename to bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py diff --git a/bec_plugins/plugins/bec_client/startup/post-startup.py b/bec_plugins/bec_client/startup/__init__.py similarity index 100% rename from bec_plugins/plugins/bec_client/startup/post-startup.py rename to bec_plugins/bec_client/startup/__init__.py diff --git a/bec_plugins/bec_client/startup/post_startup.py b/bec_plugins/bec_client/startup/post_startup.py new file mode 100644 index 0000000..3c24542 --- /dev/null +++ b/bec_plugins/bec_client/startup/post_startup.py @@ -0,0 +1,61 @@ +""" +Post startup script for the BEC client. This script is executed after the +IPython shell is started. It is used to load the beamline specific +information and to setup the prompts. + +The script is executed in the global namespace of the IPython shell. This +means that all variables defined here are available in the shell. + +If needed, bec command-line arguments can be parsed here. For example, to +parse the --session argument, add the following lines to the script: + + import argparse + parser = argparse.ArgumentParser() + parser.add_argument("--session", help="Session name", type=str, default="my_default_session") + args = parser.parse_args() + + if args.session == "my_session": + print("Loading my_session session") + from bec_plugins.bec_client.plugins.my_session import * + else: + print("Loading default session") + from bec_plugins.bec_client.plugins.default_session import * +""" + +# pylint: disable=invalid-name, unused-import, import-error, undefined-variable, unused-variable, unused-argument, no-name-in-module +import argparse + +from bec_lib.core import bec_logger + +logger = bec_logger.logger + +logger.info("Using the cSAXS startup script.") + +parser = argparse.ArgumentParser() +parser.add_argument("--session", help="Session name", type=str, default="cSAXS") +args = parser.parse_args() + +if args.session == "LamNI": + print("Loading LamNI session") + from bec_plugins.bec_client.plugins.cSAXS import * + from bec_plugins.bec_client.plugins.LamNI import * + + lamni = LamNI(bec) + +elif args.session == "cSAXS": + print("Loading cSAXS session") + from bec_plugins.bec_client.plugins.cSAXS import * + + +# SETUP BEAMLINE INFO +from bec_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo + +from bec_plugins.bec_client.plugins.cSAXS.beamline_info import BeamlineInfo + +bec._beamline_mixin._bl_info_register(BeamlineInfo) +bec._beamline_mixin._bl_info_register(SLSInfo) +bec._beamline_mixin._bl_info_register(OperatorInfo) + +# SETUP PROMPTS +bec._ip.prompts.username = args.session +bec._ip.prompts.status = 1 diff --git a/bec_plugins/bec_client/startup/pre_startup.py b/bec_plugins/bec_client/startup/pre_startup.py new file mode 100644 index 0000000..dcfa194 --- /dev/null +++ b/bec_plugins/bec_client/startup/pre_startup.py @@ -0,0 +1,25 @@ +""" +Pre-startup script for BEC client. This script is executed before the BEC client +is started. It can be used to set up the BEC client configuration. The script is +executed in the global namespace of the BEC client. This means that all +variables defined here are available in the BEC client. + +To set up the BEC client configuration, use the ServiceConfig class. For example, +to set the configuration file path, add the following lines to the script: + + import pathlib + from bec_lib.core import ServiceConfig + + current_path = pathlib.Path(__file__).parent.resolve() + CONFIG_PATH = f"{current_path}/" + + config = ServiceConfig(CONFIG_PATH) + +If this startup script defined a ServiceConfig object, the BEC client will use +it to configure itself. Otherwise, the BEC client will use the default config. +""" + +# example: +# current_path = pathlib.Path(__file__).parent.resolve() +# CONFIG_PATH = f"{current_path}/../../../bec_config.yaml" +# config = ServiceConfig(CONFIG_PATH) diff --git a/bec_plugins/plugins/bec_client/plugins/LamNI/__init__.py b/bec_plugins/plugins/bec_client/plugins/LamNI/__init__.py deleted file mode 100644 index ff9c23c..0000000 --- a/bec_plugins/plugins/bec_client/plugins/LamNI/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .load_additional_correction import lamni_read_additional_correction -from .x_ray_eye_align import LamNI, XrayEyeAlign diff --git a/bec_plugins/plugins/bec_client/startup/pre-startup.py b/bec_plugins/scan_server/__init__.py similarity index 100% rename from bec_plugins/plugins/bec_client/startup/pre-startup.py rename to bec_plugins/scan_server/__init__.py diff --git a/bec_plugins/plugins/scan_server/scan_plugins/LamNIFermatScan.py b/bec_plugins/scan_server/scan_plugins/LamNIFermatScan.py similarity index 100% rename from bec_plugins/plugins/scan_server/scan_plugins/LamNIFermatScan.py rename to bec_plugins/scan_server/scan_plugins/LamNIFermatScan.py diff --git a/bec_plugins/plugins/scibec/lamni_config.py b/bec_plugins/scibec/lamni_config.py similarity index 100% rename from bec_plugins/plugins/scibec/lamni_config.py rename to bec_plugins/scibec/lamni_config.py diff --git a/bec_plugins/plugins/scibec/test_config_cSAXS.yaml b/bec_plugins/scibec/test_config_cSAXS.yaml similarity index 100% rename from bec_plugins/plugins/scibec/test_config_cSAXS.yaml rename to bec_plugins/scibec/test_config_cSAXS.yaml diff --git a/deployment/autodeploy_version b/deployment/autodeploy_version new file mode 100644 index 0000000..2dac2c3 --- /dev/null +++ b/deployment/autodeploy_version @@ -0,0 +1,11 @@ +# This file is used to select the BEC and Ophyd Devices version for the auto deployment process. +# Do not edit this file unless you know what you are doing! + +# The version can be a git tag, branch or commit hash. + +# BEC version to use +BEC_AUTODEPLOY_VERSION="cli_launch" + +# ophyd_devices version to use +OPHYD_DEVICES_AUTODEPLOY_VERSION="master" + diff --git a/deployment/deploy.sh b/deployment/deploy.sh new file mode 100755 index 0000000..77f1a64 --- /dev/null +++ b/deployment/deploy.sh @@ -0,0 +1,24 @@ +# deployment script to be translated to Ansible +BEAMLINE_REPO=gitlab.psi.ch:bec/csaxs-bec.git + +# start redis +# docker run --network=host --name redis-bec -d redis +# alternative: +# conda install redis; redis-server & + +git clone git@$BEAMLINE_REPO + +# get the target versions for ophyd_devices and BEC +source ./csaxs-bec/deployment/autodeploy_versions + +git clone -b $OPHYD_DEVICES_AUTODEPLOY_VERSION git@gitlab.psi.ch:bec/ophyd_devices.git +git clone -b $BEC_AUTODEPLOY_VERSION git@gitlab.psi.ch:bec/bec.git + +# install BEC +cd bec +source ./bin/install_bec_dev.sh + +cd ../ +# start the BEC server +bec-server start --config ./csaxs-bec/deployment/bec-server-config.yaml +