From 96921da618e44efc53d173e420b9e2a6b2d6ca5d Mon Sep 17 00:00:00 2001 From: appel_c Date: Thu, 18 Apr 2024 09:37:58 +0200 Subject: [PATCH] feat: removed old and introduced new structure --- .git_hooks/post-commit | 3 + .git_hooks/pre-commit | 5 +- .gitignore | 3 + README.md | 69 +- .../bec_client/plugins/LamNI/LamNI_logo.png | Bin 49460 -> 0 bytes .../bec_client/plugins/LamNI/__init__.py | 2 - .../bec_client/plugins/LamNI/bl_show_all.mac | 269 ---- .../plugins/LamNI/lamni_optics_mixin.py | 161 -- .../LamNI/load_additional_correction.py | 23 - .../plugins/LamNI/x_ray_eye_align.py | 1332 ----------------- .../bec_client/plugins/cSAXS/__init__.py | 1 - .../bec_client/plugins/cSAXS/beamline_info.py | 108 -- .../plugins/cSAXS/cSAXS_beamline.py | 28 - bin/open_tunnel.sh | 5 - bin/setup_bec.sh | 47 - bin/setup_bec_widgets.sh | 16 - csaxs_bec/__init__.py | 0 csaxs_bec/bec_ipython_client/__init__.py | 0 .../high_level_interface/__init__.py | 0 .../bec_ipython_client/plugins/__init__.py | 0 .../bec_ipython_client/startup/__init__.py | 0 .../startup/post_startup.py | 76 + .../bec_ipython_client/startup/pre_startup.py | 25 + csaxs_bec/bec_widgets/__init__.py | 0 csaxs_bec/dap_services/__init__.py | 0 csaxs_bec/device_configs/__init__.py | 0 csaxs_bec/devices/__init__.py | 0 csaxs_bec/scans/__init__.py | 0 deployment/autodeploy_versions | 11 - deployment/bec-server-config.yaml | 18 - deployment/deploy.sh | 27 - pyproject.toml | 55 + setup.cfg | 21 - setup.py | 7 - 34 files changed, 174 insertions(+), 2138 deletions(-) create mode 100755 .git_hooks/post-commit delete mode 100644 bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png delete mode 100644 bec_plugins/bec_client/plugins/LamNI/__init__.py delete mode 100644 bec_plugins/bec_client/plugins/LamNI/bl_show_all.mac delete mode 100644 bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py delete mode 100755 bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py delete mode 100644 bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py delete mode 100644 bec_plugins/bec_client/plugins/cSAXS/__init__.py delete mode 100644 bec_plugins/bec_client/plugins/cSAXS/beamline_info.py delete mode 100644 bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py delete mode 100755 bin/open_tunnel.sh delete mode 100755 bin/setup_bec.sh delete mode 100755 bin/setup_bec_widgets.sh create mode 100644 csaxs_bec/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/high_level_interface/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/plugins/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/startup/__init__.py create mode 100644 csaxs_bec/bec_ipython_client/startup/post_startup.py create mode 100644 csaxs_bec/bec_ipython_client/startup/pre_startup.py create mode 100644 csaxs_bec/bec_widgets/__init__.py create mode 100644 csaxs_bec/dap_services/__init__.py create mode 100644 csaxs_bec/device_configs/__init__.py create mode 100644 csaxs_bec/devices/__init__.py create mode 100644 csaxs_bec/scans/__init__.py delete mode 100644 deployment/autodeploy_versions delete mode 100644 deployment/bec-server-config.yaml delete mode 100755 deployment/deploy.sh create mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.git_hooks/post-commit b/.git_hooks/post-commit new file mode 100755 index 0000000..3fe80fe --- /dev/null +++ b/.git_hooks/post-commit @@ -0,0 +1,3 @@ +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +semantic-release changelog -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__ +semantic-release version -D version_variable=$SCRIPT_DIR/../../semantic_release/__init__.py:__version__ \ No newline at end of file diff --git a/.git_hooks/pre-commit b/.git_hooks/pre-commit index 4dde641..392493b 100644 --- a/.git_hooks/pre-commit +++ b/.git_hooks/pre-commit @@ -1,2 +1,3 @@ -black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM) -git add $(git diff --cached --name-only --diff-filter=ACM) +black --line-length=100 $(git diff --cached --name-only --diff-filter=ACM -- '*.py') +isort --line-length=100 --profile=black --multi-line=3 --trailing-comma $(git diff --cached --name-only --diff-filter=ACM -- '*.py') +git add $(git diff --cached --name-only --diff-filter=ACM -- '*.py') diff --git a/.gitignore b/.gitignore index 74235c6..f4c73aa 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ **/.pytest_cache **/*.egg* +# recovery_config files +recovery_config_* + # file writer data **.h5 diff --git a/README.md b/README.md index 00f0a12..757f5be 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,12 @@ You might want to run cSAXS copy scripts before in case you want to have the for ## Overview -1. Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.) -2. Install BEC -3. Start Epics iocs -4. Start BEC, BEC server and load/modify the device config with relevant hardware -5. BEC commands -6. Start BEC widgets (GUI for motor control, eiger live plot) -7. Troubleshooting and common problems +- Clone cSAXS BEC repository into e-account (e.g. into ~/Data10/software/.) +- Start Epics iocs +- Start BEC, BEC server and load/modify the device config with relevant hardware +- BEC commands -## 1. Clone cSAXS BEC repository +## Clone cSAXS BEC repository Clone the current cSAXS BEC repository from GIT into the new e-account. Create directory @@ -24,19 +21,9 @@ cd ~/Data10/software ``` Clone repository ```bash -git clone https://gitlab.psi.ch/bec/csaxs-bec.git +git clone https://gitlab.psi.ch/bec/csaxs_bec.git ``` - -## 2. Install BEC - -There is a bash sript in the followin directory. -Go to the directory and run the script on pc15543 logged in as the e-account (BEC server): -```bash -ssh pc15543 -cd ~/Data10/software/csaxs-bec/bin/ -./setup_bec.sh -``` -## 3. Start epics iocs +## Start epics iocs You can start up the iocs while the *./setup_bec.sh* script is running. Be aware though that the scripts requires you to interact with it. @@ -94,7 +81,7 @@ iocsh -7.0.6 startup.script ``` Be aware -7.0.6 is referring to the current epics version and might change in future (SLS 2.0) -## 4. Start BEC, BEC server and load device config +## Start BEC, BEC server and load device config Step 1 needs to have finished for continuing with these steps. What remains now is to start the bec server. Connect to pc15543 and open a new terminal to run: @@ -127,7 +114,7 @@ bec.config.save_current_session('~/Data10/software/current_config.yaml') ``` The second command is helpful if you adjust limits of motors, which will then be stored in the config and loaded if a reload of the configuration is needed. -## 5. BEC commands +## BEC commands A number of commands that are useful: @@ -147,41 +134,3 @@ scans.line_scan(dev.samx, -1, 1, dev.samy, -1, 1, steps=20, exp_time=0.5, readou scans.sgalil_grid(start_y = , end_y = , interval_y = , start_x=, end_x=, interval_x =, exp_time=0.5, readout_time=3e-3, relative=True) ``` -## 6. Start BEC widgets (GUI for motor control, eiger live plot) - -To start the BEC widgets, the first step is to make the bec_widgets_venv using the start startup script. -Follow the commands below: -``` bash -cd ~/Data10/software/csaxs-bec/bin -./setup_bec_widgets.sh -``` -Afterwards, activate the environment on either cons-01 comp-1/2 -``` bash -cd ~/Data10/software/ -source activate bec_widgets_venv/bin/activate -``` -Each Plot needs their own shell with activate environment - -1. Eiger Plot -``` bash -cd ~/Data10/software/bec-widgets/bec_widgets/examples/eiger_plot -python eiger_plot.py -``` -2. Motor Controller -``` bash -cd ~/Data10/software/bec-widgets/bec_widgets/examples/motor_movement -python motor_example.py --config csaxs_config.yaml -``` - -## 7. Troubleshooting and common problems - -Sometimes the data backend for the Eiger gets stuck or misses frames, this will result in an error -``` python -raise EigerTimeoutError( -ophyd_devices.epics.devices.eiger9m_csaxs.EigerTimeoutError: Reached timeout with detector state 1, std_daq state FINISHED and received frames of 100 for the file writer) -``` -This happens more likely after CTRL C of a scan. To recover from this more reliably, perform the an acquisition in burst mode with 100 frames, little exposure until no error message is raised after. This can be up to 3 times from former experience. -``` bash -scans.acquire(exp_time=0.02, frames_per_trigger=100, readout_time= 3e-3) -``` -Afterwards, you should be good to continue with 2D gridscans. diff --git a/bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png b/bec_plugins/bec_client/plugins/LamNI/LamNI_logo.png deleted file mode 100644 index 965195cffb7ce55bced133089bc0f9ebaf0906ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49460 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≶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 8) { - text_bf - } - printf("%.3f mm\n",gap) - text_non_bf - - if (!_id_loss_rate_ok()) { - id_loss_rate - } - - bl_shutter_status - - if (_bl_cvd_filter_open()) { - text_bf - printf("CVD diamond filter : open / out\n") - text_non_bf - } - - if (!_bl_xbox_valve_es1_open()) { - bl_xbox_valve_es1 _show - } - - if (_bl_ln2_non_standard()) { - text_bf - printf("\nNon standard liquid nitrogen cooling-warning parameters occur. Please report this to your local contact.\n") - text_non_bf - printf("The macro bl_ln2_warn can be used to control this e-mail warning feature.\n") - bl_ln2_warn "show" - printf("\n") - } - - printf("\n") - bl_flight_tube_pressure - printf("\n") - - bl_attended _show - - _bl_check_alarm_records(1,1) - - printf("\n") - bl_op_msg -}' - - -def _bl_hall_temperature_ok() '{ - local temp_ok - local stat - - temp_ok = 1 - - # EH T02 average temperature - stat = epics_get("ILUUL-02AV:TEMP") - if ((stat < 23.0) || (stat > 26.0)) { - temp_ok = 0 - } - - # EH T02 temperature at T0204 axis 16 - stat = epics_get("ILUUL-0200-EB104:TEMP") - if ((stat < 23.0) || (stat > 26.0)) { - temp_ok = 0 - } - - # EH T02 temperature at T0205 axis 18 - stat = epics_get("ILUUL-0200-EB105:TEMP") - if ((stat < 23.0) || (stat > 26.0)) { - temp_ok = 0 - } - - return (temp_ok) -}' - - -# ---------------------------------------------------------------------- -def bl_hall_temperature '{ - local stat - - stat = epics_get("ILUUL-02AV:TEMP") - printf("hall T02 average temperature : ") - if ((stat < 23.0) || (stat > 25.0)) { - text_bf - } - printf("%.2f deg.C\n",stat) - text_non_bf - - stat = epics_get("ILUUL-0200-EB104:TEMP") - printf("hall temperature at T0204 axis 16 : ") - if ((stat < 23) || (stat > 25)) { - text_bf - } - printf("%.2f deg.C\n",stat) - text_non_bf - - stat = epics_get("ILUUL-0200-EB105:TEMP") - printf("hall temperature at T0205 axis 18 : ") - if ((stat < 23) || (stat > 25)) { - text_bf - } - printf("%.2f deg.C\n",stat) - text_non_bf - -# stat = epics_get("ILUUL-0300-EB102:TEMP") -# printf("EH T03 temperature at T0302 axis 21: ") -# if ((stat < 23) || (stat > 25)) { -# text_bf -# } -# printf("%.2f deg.C\n",stat) -# text_non_bf - -}' - -def _bl_sls_status_unusual() '{ - local unusual - local stat - - unusual = 0 - - stat = epics_get("X12SA-SR-VAC:SETPOINT") - if (stat != "OK") { - unusual = 1 - } - - stat = epics_get("ACOAU-ACCU:OP-MODE.VAL") - if ((stat != "Light Available") && (stat != "Light-Available")) { - unusual = 1 - } - - stat = epics_get("ALIRF-GUN:INJ-MODE") - if (stat != "TOP-UP") { - unusual = 1 - } - - # current threshold - stat = epics_get("ALIRF-GUN:CUR-LOWLIM") - if (stat < 350) { - unusual = 1 - } - - # current deadband - stat = epics_get("ALIRF-GUN:CUR-DBAND") - if (stat > 2) { - unusual = 1 - } - - # orbit feedback mode - stat = epics_get("ARIDI-BPM:OFB-MODE") - if (stat != "fast") { - unusual = 1 - } - - # fast orbit feedback - stat = epics_get("ARIDI-BPM:FOFBSTATUS-G") - if (stat != "running") { - unusual = 1 - } - - return(unusual) -}' - -def bl_sls_status '{ - local stat - - stat = epics_get("ACOAU-ACCU:OP-MODE.VAL") - printf("SLS status : ") - if ((stat != "Light Available") && (stat != "Light-Available")) { - text_bf - } - printf("%s\n",stat) - text_non_bf - - stat = epics_get("ALIRF-GUN:INJ-MODE") - printf("SLS injection mode : ") - if (stat != "TOP-UP") { - text_bf - } - printf("%s\n",stat) - text_non_bf - - stat = epics_get("ALIRF-GUN:CUR-LOWLIM") - printf("SLS current threshold : ") - if (stat < 350) { - text_bf - } - printf("%7.3f\n",stat) - text_non_bf - - stat = epics_get("ALIRF-GUN:CUR-DBAND") - printf("SLS current deadband : ") - if (stat > 2) { - text_bf - } - printf("%7.3f\n",stat) - text_non_bf - - stat = epics_get("ACORF-FILL:PAT-SELECT") - printf("SLS filling pattern : ") - printf("%s\n",stat) - - bl_ring_current - - stat = epics_get("ARIDI-PCT:TAU-HOUR") - printf("SLS filling life time : ") - printf("%.2f h\n",stat) - - stat = epics_get("ARIDI-BPM:OFB-MODE") - printf("orbit feedback mode : ") - if (stat != "fast") { - text_bf - } - printf("%s\n",stat) - text_non_bf - - stat = epics_get("ARIDI-BPM:FOFBSTATUS-G") - printf("fast orbit feedback : ") - if (stat != "running") { - text_bf - } - printf("%s\n",stat) - text_non_bf - -}' - -def _bl_get_ring_current() '{ - return epics_get("ARIDI-PCT:CURRENT") -}' - - -# ---------------------------------------------------------------------- -def _bl_no_ring_current() '{ - # set an arbitrary current limit of 100mA as no-beam limit - if (_bl_get_ring_current() < 100) { - return 1 - } else { - return 0 - } -}' - - -# ---------------------------------------------------------------------- -def bl_ring_current '{ - local curr - - curr = _bl_get_ring_current() - - if (curr < 300) { - text_bf - } - printf("SLS ring current : %.3f mA\n",curr) - text_non_bf -}' \ No newline at end of file diff --git a/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py b/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py deleted file mode 100644 index ccf6d95..0000000 --- a/bec_plugins/bec_client/plugins/LamNI/lamni_optics_mixin.py +++ /dev/null @@ -1,161 +0,0 @@ -import builtins -import time - -from rich import box -from rich.console import Console -from rich.table import Table - -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: - @staticmethod - def _get_user_param_safe(device, var): - param = dev[device].user_parameter - if not param or param.get(var) is None: - raise ValueError(f"Device {device} has no user parameter definition for {var}.") - return param.get(var) - - 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) - # 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 - # 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": - break - if moved_out == "n": - return - leyex_in = self._get_user_param_safe("leyex", "in") - leyey_in = self._get_user_param_safe("leyey", "in") - umv(dev.leyex, leyex_in, dev.leyey, leyey_in) - self.align.update_frame() - - def _lfzp_in(self): - loptx_in = self._get_user_param_safe("loptx", "in") - lopty_in = self._get_user_param_safe("lopty", "in") - umv( - dev.loptx, loptx_in, dev.lopty, lopty_in - ) # for 7.2567 keV and 150 mu, 60 nm fzp, loptz 83.6000 for propagation 1.4 mm - - def lfzp_in(self): - """ - move in the lamni zone plate. - This will disable rt feedback, move the FZP and re-enabled the feedback. - """ - if "rtx" in dev and dev.rtx.enabled: - dev.rtx.controller.feedback_disable() - - self._lfzp_in() - - if "rtx" in dev and dev.rtx.enabled: - dev.rtx.controller.feedback_enable_with_reset() - - def loptics_in(self): - """ - Move in the lamni optics, including the FZP and the OSA. - """ - self.lfzp_in() - self.losa_in() - - def loptics_out(self): - """Move out the lamni optics""" - if "rtx" in dev and dev.rtx.enabled: - dev.rtx.controller.feedback_disable() - - # self.lcs_out() - self.losa_out() - loptx_out = self._get_user_param_safe("loptx", "out") - lopty_out = self._get_user_param_safe("lopty", "out") - umv(dev.loptx, loptx_out, dev.lopty, lopty_out) - - if "rtx" in dev and dev.rtx.enabled: - time.sleep(1) - dev.rtx.controller.feedback_enable_with_reset() - - def lcs_in(self): - # umv lcsx -1.852 lcsy -0.095 - pass - - def lcs_out(self): - umv(dev.lcsy, 3) - - def losa_in(self): - # 6.2 keV, 170 um FZP - # umv(dev.losax, -1.4450000, dev.losay, -0.1800) - # umv(dev.losaz, -1) - # 6.7, 170 - # umv(dev.losax, -1.4850, dev.losay, -0.1930) - # umv(dev.losaz, 1.0000) - # 7.2, 150 - losax_in = self._get_user_param_safe("losax", "in") - losay_in = self._get_user_param_safe("losay", "in") - losaz_in = self._get_user_param_safe("losaz", "in") - umv(dev.losax, losax_in, dev.losay, losay_in) - umv(dev.losaz, losaz_in) - # 11 kev - # umv(dev.losax, -1.161000, dev.losay, -0.196) - # umv(dev.losaz, 1.0000) - - def losa_out(self): - losay_out = self._get_user_param_safe("losay", "out") - losaz_out = self._get_user_param_safe("losaz", "out") - umv(dev.losaz, losaz_out) - umv(dev.losay, losay_out) - - def lfzp_info(self): - loptz_val = dev.loptz.read()["loptz"]["value"] - distance = -loptz_val + 85.6 + 52 - print(f"The sample is in a distance of {distance:.1f} mm from the FZP.") - - 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() - table = Table( - title=f"At the current energy of {mokev_val:.4f} keV we have following options:", - box=box.SQUARE, - ) - table.add_column("Diameter", justify="center") - table.add_column("Focal distance", justify="center") - table.add_column("Current beam size", justify="center") - - wavelength = 1.2398e-9 / mokev_val - - for diameter in diameters: - outermost_zonewidth = 60e-9 - focal_distance = diameter * outermost_zonewidth / wavelength - beam_size = ( - -diameter / (focal_distance * 1000) * (focal_distance * 1000 - distance) * 1e6 - ) - table.add_row( - f"{diameter*1e6:.2f} microns", - f"{focal_distance:.2f} mm", - f"{beam_size:.2f} microns", - ) - - console.print(table) - - print("OSA Information:") - # print(f"Current losaz %.1f\n", A[losaz]) - # print("The OSA will collide with the sample plane at %.1f\n\n", 89.3-A[loptz]) - print( - "The numbers presented here are for a sample in the plane of the lamni sample holder.\n" - ) diff --git a/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py b/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py deleted file mode 100755 index 3fd1281..0000000 --- a/bec_plugins/bec_client/plugins/LamNI/load_additional_correction.py +++ /dev/null @@ -1,23 +0,0 @@ -def lamni_read_additional_correction(): - # "additional_correction_shift" - # [0][] x , [1][] y, [2][] angle, [3][0] number of elements - import numpy as np - - with open("correction_lamni_um_S01405_.txt", "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(value) - elif name == "corr_pos_y": - corr_pos_y.append(value) - elif name == "corr_angle": - corr_angle.append(value) - return (corr_pos_x, corr_pos_y, corr_angle, num_elements) diff --git a/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py b/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py deleted file mode 100644 index 17a6f25..0000000 --- a/bec_plugins/bec_client/plugins/LamNI/x_ray_eye_align.py +++ /dev/null @@ -1,1332 +0,0 @@ -import builtins -import datetime -import os -import subprocess -import threading -import time -from collections import defaultdict -from pathlib import Path - -import h5py -import numpy as np -from bec_client.plugins.cSAXS import epics_get, epics_put, fshclose, fshopen -from bec_lib import bec_logger -from bec_lib.alarm_handler import AlarmBase -from bec_lib.pdf_writer import PDFWriter -from typeguard import typechecked - -from .lamni_optics_mixin import LamNIOpticsMixin - -logger = bec_logger.logger -bec = builtins.__dict__.get("bec") - - -class XrayEyeAlign: - # pixel calibration, multiply to get mm - # PIXEL_CALIBRATION = 0.2/209 #.2 with binning - PIXEL_CALIBRATION = 0.2 / 218 # .2 with binning - - def __init__(self, client, lamni) -> None: - self.client = client - self.lamni = lamni - self.device_manager = client.device_manager - self.scans = client.scans - self.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] - - def save_frame(self): - epics_put("XOMNYI-XEYE-SAVFRAME:0", 1) - - def update_frame(self): - epics_put("XOMNYI-XEYE-ACQDONE:0", 0) - # start live - epics_put("XOMNYI-XEYE-ACQ:0", 1) - # wait for start live - while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: - time.sleep(0.5) - print("waiting for live view to start...") - fshopen() - - epics_put("XOMNYI-XEYE-ACQDONE:0", 0) - - while epics_get("XOMNYI-XEYE-ACQDONE:0") == 0: - print("waiting for new frame...") - time.sleep(0.5) - - time.sleep(0.5) - # stop live view - epics_put("XOMNYI-XEYE-ACQ:0", 0) - time.sleep(1) - # fshclose - print("got new frame") - - def _disable_rt_feedback(self): - self.device_manager.devices.rtx.controller.feedback_disable() - - def _enable_rt_feedback(self): - self.device_manager.devices.rtx.controller.feedback_enable_with_reset() - - def tomo_rotate(self, val: float): - # pylint: disable=undefined-variable - umv(self.device_manager.devices.lsamrot, val) - - def get_tomo_angle(self): - return self.device_manager.devices.lsamrot.readback.read()["lsamrot"]["value"] - - def update_fov(self, k: int): - self._xray_fov_xy[0] = max(epics_get(f"XOMNYI-XEYE-XWIDTH_X:{k}"), self._xray_fov_xy[0]) - self._xray_fov_xy[1] = max(0, self._xray_fov_xy[0]) - - @property - def movement_buttons_enabled(self): - return [epics_get("XOMNYI-XEYE-ENAMVX:0"), epics_get("XOMNYI-XEYE-ENAMVY:0")] - - @movement_buttons_enabled.setter - def movement_buttons_enabled(self, enabled: bool): - enabled = int(enabled) - epics_put("XOMNYI-XEYE-ENAMVX:0", enabled) - epics_put("XOMNYI-XEYE-ENAMVY:0", enabled) - - def send_message(self, msg: str): - epics_put("XOMNYI-XEYE-MESSAGE:0.DESC", msg) - - 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() - - epics_put("XOMNYI-XEYE-PIXELSIZE:0", self.PIXEL_CALIBRATION) - - self._enable_rt_feedback() - - # initialize - # disable movement buttons - self.movement_buttons_enabled = False - - epics_put("XOMNYI-XEYE-ACQ:0", 0) - self.send_message("please wait...") - - # put sample name - epics_put("XOMNYI-XEYE-SAMPLENAME:0.DESC", "Let us LAMNI...") - - # first step - self._disable_rt_feedback() - k = 0 - - # move zone plate in, eye in to get beam position - self.lamni.lfzp_in() - - self.update_frame() - - # enable submit buttons - self.movement_buttons_enabled = False - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - epics_put("XOMNYI-XEYE-STEP:0", 0) - self.send_message("Submit center value of FZP.") - - while True: - if epics_get("XOMNYI-XEYE-SUBMIT:0") == 1: - val_x = epics_get(f"XOMNYI-XEYE-XVAL_X:{k}") * self.PIXEL_CALIBRATION # in mm - val_y = epics_get(f"XOMNYI-XEYE-YVAL_Y:{k}") * self.PIXEL_CALIBRATION # in mm - self.alignment_values[k] = [val_x, val_y] - print( - f"Clicked position {k}: x {self.alignment_values[k][0]}, y {self.alignment_values[k][1]}" - ) - - if k == 0: # received center value of FZP - self.send_message("please wait ...") - # perform movement: FZP out, Sample in - self.lamni.loptics_out() - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - self.movement_buttons_enabled = False - print("Moving sample in, FZP out") - - self._disable_rt_feedback() - time.sleep(0.3) - self._enable_rt_feedback() - time.sleep(0.3) - - # zero is now at the center - self.update_frame() - self.send_message("Go and find the sample") - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - self.movement_buttons_enabled = True - - elif ( - k == 1 - ): # received sample center value at samroy 0 ie the final base shift values - 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 - self.shift_xy[1] += ( - self.alignment_values[1][1] - self.alignment_values[0][1] - ) * 1000 - print( - f"Base shift values from movement and clicked position are x {self.shift_xy[0]}, y {self.shift_xy[1]}" - ) - - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - - self.send_message("please wait ...") - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - self.movement_buttons_enabled = False - time.sleep(1) - - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - - epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) - self.update_frame() - self.send_message("Submit sample center and FOV (0 deg)") - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - self.update_fov(k) - - elif 1 < k < 10: # received sample center value at samroy 0 ... 315 - self.send_message("please wait ...") - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - - # we swtich feedback off before rotating to not have it on and off again later for smooth operation - self._disable_rt_feedback() - self.tomo_rotate((k - 1) * 45 - 45 / 2) - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - self._disable_rt_feedback() - self.tomo_rotate((k - 1) * 45) - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - - epics_put("XOMNYI-XEYE-ANGLE:0", self.get_tomo_angle()) - self.update_frame() - self.send_message("Submit sample center") - epics_put("XOMNYI-XEYE-SUBMIT:0", 0) - epics_put("XOMNYI-XEYE-ENAMVX:0", 1) - self.update_fov(k) - - elif k == 10: # received sample center value at samroy 270 and done - self.send_message("done...") - epics_put("XOMNYI-XEYE-SUBMIT:0", -1) # disable submit button - self.movement_buttons_enabled = False - self.update_fov(k) - break - - k += 1 - epics_put("XOMNYI-XEYE-STEP:0", k) - if k < 2: - # allow movements, store movements to calculate center - _xrayeyalignmvx = epics_get("XOMNYI-XEYE-MVX:0") - _xrayeyalignmvy = epics_get("XOMNYI-XEYE-MVY:0") - if _xrayeyalignmvx != 0 or _xrayeyalignmvy != 0: - self.shift_xy[0] = self.shift_xy[0] + _xrayeyalignmvx - self.shift_xy[1] = self.shift_xy[1] + _xrayeyalignmvy - self.scans.lamni_move_to_scan_center( - self.shift_xy[0] / 1000, - self.shift_xy[1] / 1000, - self.get_tomo_angle(), - ).wait() - print( - f"Current center horizontal {self.shift_xy[0]} vertical {self.shift_xy[1]}" - ) - epics_put("XOMNYI-XEYE-MVY:0", 0) - epics_put("XOMNYI-XEYE-MVX:0", 0) - self.update_frame() - - time.sleep(0.2) - - self.write_output() - fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2 - fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2 - print( - f"The largest field of view from the xrayeyealign was \nfovx = {fovx:.0f} microns, fovy = {fovy:.0f} microns" - ) - print("Use matlab routine to fit the current alignment...") - - print( - f"This additional shift is applied to the base shift values\n which are x {self.shift_xy[0]}, y {self.shift_xy[1]}" - ) - - self._disable_rt_feedback() - - self.tomo_rotate(0) - - print( - "\n\nNEXT LOAD ALIGNMENT PARAMETERS\nby running lamni.align.read_xray_eye_correction()\n" - ) - - self.client.set_global_var("tomo_fov_offset", self.shift_xy) - - def write_output(self): - with open( - os.path.expanduser("~/Data10/specES1/internal/xrayeye_alignmentvalues"), "w" - ) as alignment_values_file: - alignment_values_file.write(f"angle\thorizontal\tvertical\n") - for k in range(2, 11): - fovx_offset = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000 - fovy_offset = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000 - print( - f"Writing to file new alignment: number {k}, value x {fovx_offset}, y {fovy_offset}" - ) - alignment_values_file.write(f"{(k-2)*45}\t{fovx_offset}\t{fovy_offset}\n") - - 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() - - with open(os.path.join(dir_path, "ptychotomoalign_Bx.txt"), "r") as file: - tomo_fit_xray_eye[0][1] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Cx.txt"), "r") as file: - tomo_fit_xray_eye[0][2] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Ay.txt"), "r") as file: - tomo_fit_xray_eye[1][0] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_By.txt"), "r") as file: - tomo_fit_xray_eye[1][1] = file.readline() - - with open(os.path.join(dir_path, "ptychotomoalign_Cy.txt"), "r") as file: - tomo_fit_xray_eye[1][2] = file.readline() - - self.client.set_global_var("tomo_fit_xray_eye", tomo_fit_xray_eye.tolist()) - # x amp, phase, offset, y amp, phase, offset - # 0 0 0 1 0 2 1 0 1 1 1 2 - - print("New alignment parameters loaded from X-ray eye") - print( - f"X Amplitude {tomo_fit_xray_eye[0][0]}," - f"X Phase {tomo_fit_xray_eye[0][1]}, " - f"X Offset {tomo_fit_xray_eye[0][2]}," - f"Y Amplitude {tomo_fit_xray_eye[1][0]}," - f"Y Phase {tomo_fit_xray_eye[1][1]}," - f"Y Offset {tomo_fit_xray_eye[1][2]}" - ) - - def lamni_compute_additional_correction_xeye_mu(self, angle): - 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.check_shutter = True - self.check_light_available = True - self.check_fofb = True - self._check_msgs = [] - 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 - - def get_beamline_checks_enabled(self): - print( - f"Shutter: {self.check_shutter}\nFOFB: {self.check_fofb}\nLight available: {self.check_light_available}" - ) - - @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() - - def set_special_angles(self, angles: list, repeats: int = 20, tolerance: float = 0.5): - """Set the special angles for a tomo - - 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. - - """ - self.special_angles = angles - self.special_angle_repeats = repeats - self.special_angle_tolerance = tolerance - - 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): - val = self.client.get_global_var("tomo_shellstep") - if val is None: - return 1 - return val - - @tomo_shellstep.setter - def tomo_shellstep(self, val: float): - self.client.set_global_var("tomo_shellstep", val) - - @property - def tomo_circfov(self): - val = self.client.get_global_var("tomo_circfov") - if val is None: - return 0.0 - return val - - @tomo_circfov.setter - def tomo_circfov(self, val: float): - self.client.set_global_var("tomo_circfov", val) - - @property - def tomo_countingtime(self): - val = self.client.get_global_var("tomo_countingtime") - if val is None: - return 0.1 - return val - - @tomo_countingtime.setter - def tomo_countingtime(self, val: float): - self.client.set_global_var("tomo_countingtime", val) - - @property - def manual_shift_x(self): - val = self.client.get_global_var("manual_shift_x") - if val is None: - return 0.0 - return val - - @manual_shift_x.setter - def manual_shift_x(self, val: float): - self.client.set_global_var("manual_shift_x", val) - - @property - def manual_shift_y(self): - val = self.client.get_global_var("manual_shift_y") - if val is None: - return 0.0 - return val - - @manual_shift_y.setter - def manual_shift_y(self, val: float): - self.client.set_global_var("manual_shift_y", val) - - @property - def lamni_piezo_range_x(self): - val = self.client.get_global_var("lamni_piezo_range_x") - if val is None: - return 20 - return val - - @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) - - @property - def lamni_piezo_range_y(self): - val = self.client.get_global_var("lamni_piezo_range_y") - if val is None: - return 20 - return val - - @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") - if val is None: - return 0 - return val - - @lamni_stitch_x.setter - @typechecked - def lamni_stitch_x(self, val: int): - self.client.set_global_var("lamni_stitch_x", val) - - @property - def lamni_stitch_y(self): - val = self.client.get_global_var("lamni_stitch_y") - if val is None: - return 0 - return val - - @lamni_stitch_y.setter - @typechecked - def lamni_stitch_y(self, val: int): - self.client.set_global_var("lamni_stitch_y", val) - - @property - def ptycho_reconstruct_foldername(self): - val = self.client.get_global_var("ptycho_reconstruct_foldername") - if val is None: - return "ptycho_reconstruct" - return val - - @ptycho_reconstruct_foldername.setter - def ptycho_reconstruct_foldername(self, val: str): - self.client.set_global_var("ptycho_reconstruct_foldername", val) - - @property - def tomo_angle_stepsize(self): - val = self.client.get_global_var("tomo_angle_stepsize") - if val is None: - return 10.0 - return val - - @tomo_angle_stepsize.setter - def tomo_angle_stepsize(self, val: float): - self.client.set_global_var("tomo_angle_stepsize", val) - - @property - def tomo_stitch_overlap(self): - val = self.client.get_global_var("tomo_stitch_overlap") - if val is None: - return 0.2 - return val - - @tomo_stitch_overlap.setter - 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") - - 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 = [] - - for stitch_x in range(-self.lamni_stitch_x, self.lamni_stitch_x + 1): - for stitch_y in range(-self.lamni_stitch_y, self.lamni_stitch_y + 1): - # pylint: disable=undefined-variable - 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.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.align.tomo_fovx_offset, - center_y=self.align.tomo_fovy_offset, - shift_x=( - 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] - - 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 _run_beamline_checks(self): - msgs = [] - dev = builtins.__dict__.get("dev") - try: - if self.check_shutter: - shutter_val = dev.x12sa_es1_shutter_status.read(cached=True) - if shutter_val["value"].lower() != "open": - self._beam_is_okay = False - 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", - ]: - self._beam_is_okay = False - msgs.append("Check beam failed: Light not available.") - if self.check_fofb: - fast_orbit_feedback = dev.sls_fast_orbit_feedback.read(cached=True) - if fast_orbit_feedback["value"] != "running": - self._beam_is_okay = False - msgs.append("Check beam failed: Fast orbit feedback is not running.") - except Exception: - logger.warning("Failed to check beam.") - return msgs - - def _check_beam(self): - while not self._stop_beam_check_event.is_set(): - self._check_msgs = self._run_beamline_checks() - - if not self._beam_is_okay: - self._stop_beam_check_event.set() - time.sleep(1) - - def _start_beam_check(self): - self._beam_is_okay = True - self._stop_beam_check_event = threading.Event() - - self.beam_check_thread = threading.Thread(target=self._check_beam, daemon=True) - self.beam_check_thread.start() - - def _was_beam_okay(self): - self._stop_beam_check_event.set() - self.beam_check_thread.join() - return self._beam_is_okay - - def _print_beamline_checks(self): - for msg in self._check_msgs: - logger.warning(msg) - - def _wait_for_beamline_checks(self): - self._print_beamline_checks() - try: - 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"]) - self.client.logbook.send_logbook_message(msg) - except Exception: - logger.warning("Failed to send update to SciLog.") - - while True: - self._beam_is_okay = True - self._check_msgs = self._run_beamline_checks() - if self._beam_is_okay: - break - self._print_beamline_checks() - time.sleep(1) - - try: - msg = bec.logbook.LogbookMessage() - msg.add_text( - f"

Operation resumed at {str(datetime.datetime.now())}.

" - ).add_tag(["BEC", "beam_check"]) - self.client.logbook.send_logbook_message(msg) - except Exception: - logger.warning("Failed to send update to SciLog.") - - def add_sample_database( - 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( - f"wget --user=omny --password=samples -q -O /tmp/currsamplesnr.txt 'https://omny.web.psi.ch/samples/newmeasurement.php?sample={samplename}&date={date}&eaccount={eaccount}&scannr={scan_number}&setup={setup}&additional={sample_additional_info}&user={user}'", - shell=True, - ) - with open("/tmp/currsamplesnr.txt") as tomo_number_file: - 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: - start_angle = 0 - elif subtomo_number == 2: - start_angle = self.tomo_angle_stepsize / 8.0 * 4 - elif subtomo_number == 3: - start_angle = self.tomo_angle_stepsize / 8.0 * 2 - elif subtomo_number == 4: - start_angle = self.tomo_angle_stepsize / 8.0 * 6 - elif subtomo_number == 5: - start_angle = self.tomo_angle_stepsize / 8.0 * 1 - elif subtomo_number == 6: - start_angle = self.tomo_angle_stepsize / 8.0 * 5 - elif subtomo_number == 7: - start_angle = self.tomo_angle_stepsize / 8.0 * 3 - 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 + 360 - for angle in np.linspace( - start_angle + _tomo_shift_angles, - angle_end, - num=int(360 / self.tomo_angle_stepsize) + 1, - 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() - 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() 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 _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( - 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: - 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""" - print("Current settings:") - print(f"Counting time = {self.tomo_countingtime} s") - print(f"Stepsize microns = {self.tomo_shellstep}") - print( - f"Piezo range (max 80) = {self.lamni_piezo_range_x}, {self.lamni_piezo_range_y}" - ) - print(f"Stitching number x,y = {self.lamni_stitch_x}, {self.lamni_stitch_y}") - print(f"Stitching overlap = {self.tomo_stitch_overlap}") - print(f"Circuilar FOV diam = {self.tomo_circfov}") - print(f"Reconstruction queue name = {self.ptycho_reconstruct_foldername}") - print( - "For information, fov offset is rotating and finding the ROI, manual shift moves rotation center" - ) - 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}") - print(f"Sample name: {self.sample_name}\n") - - user_input = input("Are these parameters correctly set for your scan? ") - if user_input == "y": - print("good then") - else: - self.tomo_countingtime = self._get_val(" s", self.tomo_countingtime, float) - self.tomo_shellstep = self._get_val(" um", self.tomo_shellstep, float) - self.lamni_piezo_range_x = self._get_val( - " um", self.lamni_piezo_range_x, float - ) - self.lamni_piezo_range_y = self._get_val( - " um", self.lamni_piezo_range_y, float - ) - self.lamni_stitch_x = self._get_val("", self.lamni_stitch_x, int) - self.lamni_stitch_y = self._get_val("", self.lamni_stitch_y, int) - self.tomo_circfov = self._get_val(" um", self.tomo_circfov, float) - self.ptycho_reconstruct_foldername = self._get_val( - "Reconstruction queue ", self.ptycho_reconstruct_foldername, str - ) - tomo_numberofprojections = self._get_val( - "Number of projections", 360 / self.tomo_angle_stepsize * 8, int - ) - - 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): - return data_type(input(f"{msg} ({default_value}): ") or default_value) - - def tomo_reconstruct(self, base_path="~/Data10/specES1"): - """write the tomo reconstruct file for the reconstruction queue""" - bec = builtins.__dict__.get("bec") - base_path = os.path.expanduser(base_path) - ptycho_queue_path = Path(os.path.join(base_path, self.ptycho_reconstruct_foldername)) - ptycho_queue_path.mkdir(parents=True, exist_ok=True) - - # pylint: disable=undefined-variable - last_scan_number = bec.queue.next_scan_number - 1 - ptycho_queue_file = os.path.abspath( - os.path.join(ptycho_queue_path, f"scan_{last_scan_number:05d}.dat") - ) - with open(ptycho_queue_file, "w") as queue_file: - scans = " ".join([str(scan) for scan in self._current_scan_list]) - queue_file.write(f"p.scan_number {scans}\n") - queue_file.write(f"p.check_nextscan_started 1\n") - - def write_pdf_report(self): - """create and write the pdf report with the current LamNI settings""" - dev = builtins.__dict__.get("dev") - header = ( - " \n" * 3 - + " ::: ::: ::: ::: :::: ::: ::::::::::: \n" - + " :+: :+: :+: :+:+: :+:+: :+:+: :+: :+: \n" - + " +:+ +:+ +:+ +:+ +:+:+ +:+ :+:+:+ +:+ +:+ \n" - + " +#+ +#++:++#++: +#+ +:+ +#+ +#+ +:+ +#+ +#+ \n" - + " +#+ +#+ +#+ +#+ +#+ +#+ +#+#+# +#+ \n" - + " #+# #+# #+# #+# #+# #+# #+#+# #+# \n" - + " ########## ### ### ### ### ### #### ########### \n" - ) - padding = 20 - 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) - 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", - f"{'e-account:':<{padding}}{str(self.client.username):>{padding}}\n", - 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)['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"{'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", - ] - 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) - 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", - 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/cSAXS/__init__.py b/bec_plugins/bec_client/plugins/cSAXS/__init__.py deleted file mode 100644 index aff5e6b..0000000 --- a/bec_plugins/bec_client/plugins/cSAXS/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .cSAXS_beamline import fshopen, fshclose, fshstatus, epics_get, epics_put diff --git a/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py b/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py deleted file mode 100644 index 49e6647..0000000 --- a/bec_plugins/bec_client/plugins/cSAXS/beamline_info.py +++ /dev/null @@ -1,108 +0,0 @@ -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/bec_client/plugins/cSAXS/cSAXS_beamline.py b/bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py deleted file mode 100644 index 6863e46..0000000 --- a/bec_plugins/bec_client/plugins/cSAXS/cSAXS_beamline.py +++ /dev/null @@ -1,28 +0,0 @@ -import epics - - -def epics_put(channel, value): - epics.caput(channel, value) - - -def epics_get(channel): - return epics.caget(channel) - - -def fshon(): - pass - - -def fshopen(): - """open the fast shutter""" - epics_put("X12SA-ES1-TTL:OUT_01", 1) - - -def fshclose(): - """close the fast shutter""" - epics_put("X12SA-ES1-TTL:OUT_01", 0) - - -def fshstatus(): - """show the fast shutter status""" - return epics_get("X12SA-ES1-TTL:OUT_01") diff --git a/bin/open_tunnel.sh b/bin/open_tunnel.sh deleted file mode 100755 index 966ae40..0000000 --- a/bin/open_tunnel.sh +++ /dev/null @@ -1,5 +0,0 @@ - -for i in `seq 1 8` -do - ssh -N -R 6379:localhost:6379 x12sa-cn-$i & -done diff --git a/bin/setup_bec.sh b/bin/setup_bec.sh deleted file mode 100755 index 4982e12..0000000 --- a/bin/setup_bec.sh +++ /dev/null @@ -1,47 +0,0 @@ - -if [ "pc15543.psi.ch" != "$(hostname)" ]; then - echo "Please run this script on pc15543" - exit 1 -fi - -module add psi-python311/2024.02 -echo module add tmux/3.2 >> ~/.bashrc -echo module add redis/7.0.12 >> ~/.bashrc - -source ~/.bashrc - -cd ~/Data10 -mkdir -p software/ -mkdir -p ~/bec/scripts -cd software - -git clone https://gitlab.psi.ch/bec/bec.git -git clone https://gitlab.psi.ch/bec/ophyd_devices.git -git clone https://gitlab.psi.ch/bec/bec-widgets.git - -python -m venv ./bec_venv -source ./bec_venv/bin/activate - -cd bec -git checkout sastt-online-changes -pip install -e ./bec_server[dev] - -cd ../csaxs-bec -pip install -e .[dev] - -#redis-server --protected-mode no & - -read -p "Do you want to set the current BEC account to $(whoami)? (yes/no) " yn - -val=$(whoami) - -case $yn in - yes ) echo ok, setting account to $val; - redis-cli SET internal/account:val $val;; - no ) echo ;; - * ) echo invalid response; - exit 1;; -esac - - -$(pwd)/open_tunnel.sh diff --git a/bin/setup_bec_widgets.sh b/bin/setup_bec_widgets.sh deleted file mode 100755 index d142791..0000000 --- a/bin/setup_bec_widgets.sh +++ /dev/null @@ -1,16 +0,0 @@ -module add psi-python311/2024.02 - -cd ~/Data10/software -python -m venv ./bec_widgets_venv -source ./bec_widgets_venv/bin/activate -pip install --upgrade pip -cd ~/Data10/software/bec/bec_lib -pip install -e . - -cd ~/Data10/software/csaxs-bec -pip install -e . - -cd ~/Data10/software/bec-widgets -pip install -e . - -echo "For the moment widgets only run on beamline consoles comp1/2 and cons1" diff --git a/csaxs_bec/__init__.py b/csaxs_bec/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/__init__.py b/csaxs_bec/bec_ipython_client/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/high_level_interface/__init__.py b/csaxs_bec/bec_ipython_client/high_level_interface/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/plugins/__init__.py b/csaxs_bec/bec_ipython_client/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/startup/__init__.py b/csaxs_bec/bec_ipython_client/startup/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/bec_ipython_client/startup/post_startup.py b/csaxs_bec/bec_ipython_client/startup/post_startup.py new file mode 100644 index 0000000..ca2ade5 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/startup/post_startup.py @@ -0,0 +1,76 @@ +""" +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_ipython_client.plugins.my_session import * + else: + print("Loading default session") + from bec_plugins.bec_ipython_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 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 csaxs_bec.bec_ipython_client.plugins.cSAXS import * + from csaxs_bec.bec_ipython_client.plugins.LamNI import * + + lamni = LamNI(bec) + +elif args.session == "cSAXS": + print("Loading cSAXS session") + from csaxs_bec.bec_ipython_client.plugins.cSAXS import * + + +# SETUP BEAMLINE INFO +from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo + +from csaxs_bec.bec_ipython_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 + + +# REGISTER BEAMLINE CHECKS +from bec_lib.bl_conditions import ( + FastOrbitFeedbackCondition, + LightAvailableCondition, + ShutterCondition, +) + +# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback) +_light_available_condition = LightAvailableCondition(dev.sls_machine_status) +_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status) +# bec.bl_checks.register(_fast_orbit_feedback_condition) +bec.bl_checks.register(_light_available_condition) +bec.bl_checks.register(_shutter_condition) diff --git a/csaxs_bec/bec_ipython_client/startup/pre_startup.py b/csaxs_bec/bec_ipython_client/startup/pre_startup.py new file mode 100644 index 0000000..dcfa194 --- /dev/null +++ b/csaxs_bec/bec_ipython_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/csaxs_bec/bec_widgets/__init__.py b/csaxs_bec/bec_widgets/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/dap_services/__init__.py b/csaxs_bec/dap_services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/device_configs/__init__.py b/csaxs_bec/device_configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/devices/__init__.py b/csaxs_bec/devices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/csaxs_bec/scans/__init__.py b/csaxs_bec/scans/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/deployment/autodeploy_versions b/deployment/autodeploy_versions deleted file mode 100644 index 44c018c..0000000 --- a/deployment/autodeploy_versions +++ /dev/null @@ -1,11 +0,0 @@ -# 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="master" - -# ophyd_devices version to use -OPHYD_DEVICES_AUTODEPLOY_VERSION="master" - diff --git a/deployment/bec-server-config.yaml b/deployment/bec-server-config.yaml deleted file mode 100644 index 6484ecb..0000000 --- a/deployment/bec-server-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -redis: - host: localhost - port: 6379 -mongodb: - host: localhost - port: 27017 -scibec: - host: http://[::1] - port: 3030 - beamline: "CSAXS" -service_config: - general: - reset_queue_on_cancel: True - enforce_ACLs: False - file_writer: - plugin: default_NeXus_format - base_path: ./ - diff --git a/deployment/deploy.sh b/deployment/deploy.sh deleted file mode 100755 index 799ff9f..0000000 --- a/deployment/deploy.sh +++ /dev/null @@ -1,27 +0,0 @@ -# deployment script to be translated to Ansible - -# NOT NEEDED since the beamline repo will be autodeployed -# BEAMLINE_REPO=gitlab.psi.ch:bec/csaxs-bec.git -# git clone git@$BEAMLINE_REPO - -module add psi-python311/2024.02 - -# start redis -docker run --network=host --name redis-bec -d redis -# alternative: -# conda install -y redis; redis-server & - - -# get the target versions for ophyd_devices and BEC -source ./csaxs-bec/deployment/autodeploy_versions - -git clone -b $OPHYD_DEVICES_AUTODEPLOY_VERSION https://gitlab.psi.ch/bec/ophyd_devices.git -git clone -b $BEC_AUTODEPLOY_VERSION https://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 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..017a063 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,55 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "csaxs_bec" +version = "0.0.0" +description = "Custom device implementations based on the ophyd hardware abstraction layer" +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 3 - Alpha", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", +] +dependencies = ["bec_ipython_client", "bec_lib", "bec_server", "rich", "pyepics"] + +[project.optional-dependencies] +dev = ["black", "isort", "coverage", "pylint", "pytest", "pytest-random-order"] + +[project.entry-points."bec"] +plugin_bec = "csaxs_bec" + +[project.entry-points."bec.scans"] +plugin_scans = "csaxs_bec.scans" + +[project.entry-points."bec.ipython_client"] +plugin_ipython_client = "csaxs_bec.bec_ipython_client" + +[project.entry-points."bec.widgets"] +plugin_widgets = "csaxs_bec.bec_widgets" + +[tool.hatch.build.targets.wheel] +include = ["*"] + +[tool.isort] +profile = "black" +line_length = 100 +multi_line_output = 3 +include_trailing_comma = true + +[tool.black] +line-length = 100 +skip-magic-trailing-comma = true + +[tool.pylint.basic] +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs = [ + ".*scanID.*", + ".*RID.*", + ".*pointID.*", + ".*ID.*", + ".*_2D.*", + ".*_1D.*", +] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index a5ae6c0..0000000 --- a/setup.cfg +++ /dev/null @@ -1,21 +0,0 @@ -[metadata] -name = bec_plugins -description = BEC plugins to modify the behaviour of services within the BEC framework -long_description = file: README.md -long_description_content_type = text/markdown -url = https://gitlab.psi.ch/bec/bec -project_urls = - Bug Tracker = https://gitlab.psi.ch/bec/bec/issues -classifiers = - Programming Language :: Python :: 3 - Development Status :: 3 - Alpha - Topic :: Scientific/Engineering - -[options] -package_dir = - = . -packages = find: -python_requires = >=3.10 - -[options.packages.find] -where = . diff --git a/setup.py b/setup.py deleted file mode 100644 index c7bb35c..0000000 --- a/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup - -if __name__ == "__main__": - setup( - install_requires=["pyyaml", "pyepics"], - extras_require={"dev": ["pytest", "pytest-random-order", "coverage"]}, - )