1615 lines
68 KiB
Python
1615 lines
68 KiB
Python
import copy
|
|
import json
|
|
import secrets
|
|
import time
|
|
from datetime import datetime
|
|
from math import ceil
|
|
from typing import List, Tuple, Optional
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import redis
|
|
|
|
import aaredaqlib.face_detection as fd
|
|
from aaredaq import workflows
|
|
from aaredaq.aaredb import AareWrapper
|
|
from aaredaq.autofocus import calculate_focus_measure
|
|
from aaredaq.config import BeamlineConfig, ABR_POS_MOUNT
|
|
from aaredaq.config import BeamlineStateEnum
|
|
from aaredaq.devices import BeamlineDevices
|
|
from aaredaq.mlbox import MlBox
|
|
from aaredaqlib.beamline import MXBeamline
|
|
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
|
|
from aaredaqlib.diffraction_geometry import DiffractionGeometry
|
|
from aaredaqlib.find_xtal import raster_centre_of_mass, create_quality_filtered_array, identify_crystal_raster, \
|
|
get_result_list_from_com, get_best_b_factor, get_best_res, get_xtal_size
|
|
from aaredaqlib.logger_config import setup_logger
|
|
from aaredaqlib.models import (
|
|
SampleShortInfo,
|
|
PuckLoadedInfo,
|
|
SampleShortInfoList,
|
|
DAQStatusModel, BeamlineStatus, SessionStatus, SampleCameraSettings, AutofocusSettings, ZoomModeEnum, CrystalSize,
|
|
SimpleScanParameters, MLBoxModel, FluorescenceSpectrumParameterModel,
|
|
FluorescenceSpectrumOutputModel)
|
|
from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid, CompletedRasterGridElem
|
|
from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan
|
|
from aaredaqlib.sample_geometry import SampleGeometryModel
|
|
from mxlibs3.jfjoch import JFJochWrapper
|
|
|
|
logger = setup_logger("aareDAQ", "/tmp/mxlogs")
|
|
|
|
class TransformationInvalidException(Exception):
|
|
def __init__(self, message="Transformation is not implemented"):
|
|
super().__init__(message)
|
|
self.message = message
|
|
logger.error(f"{message}", extra={"exception:" : Exception})
|
|
|
|
def __str__(self):
|
|
return self.message
|
|
|
|
|
|
class LoopCenteringFailed(Exception):
|
|
#logger.error(f"Loop centering failed", extra={"exception:" : Exception})
|
|
pass
|
|
|
|
|
|
class AareDAQ:
|
|
def __init__(self, cfg: BeamlineConfig, bl: MXBeamline):
|
|
self.last_time = 0.0
|
|
self.__cfg = cfg
|
|
self.__devs = BeamlineDevices(bl)
|
|
self.__mlbox = MlBox()
|
|
self.__jfjoch = JFJochWrapper(bl)
|
|
self.__bl = bl.value.upper()
|
|
self.__aare = AareWrapper(bl)
|
|
self.__saved_box = None
|
|
|
|
@property
|
|
def state(self) -> BeamlineStateEnum:
|
|
return self.__cfg.state
|
|
|
|
@property
|
|
def busy(self) -> bool:
|
|
return self.__cfg.state_busy
|
|
|
|
@state.setter
|
|
def state(self, target: BeamlineStateEnum):
|
|
if target == BeamlineStateEnum.Moving:
|
|
logger.error(f"Cannot explicitly move to busy state", extra={"target":target, "state":self.__cfg.state})
|
|
raise Exception("Cannot explicitly move to busy state")
|
|
|
|
start = time.perf_counter()
|
|
self.__cfg.try_set_busy(timeout=300)
|
|
self.__set_state(target)
|
|
self.__cfg.state_busy = False
|
|
|
|
end = time.perf_counter()
|
|
|
|
self.last_time = end - start
|
|
|
|
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str|None]:
|
|
file_prefix = None
|
|
|
|
if self.status.sample is None:
|
|
return None, file_prefix
|
|
|
|
logger.debug(f"generate params for sample: {self.status.sample}")
|
|
aaredb_params = self.status.sample.aaredb_params if hasattr(self.status.sample, "aaredb_params") else None
|
|
if aaredb_params is None:
|
|
return None, file_prefix
|
|
|
|
#if aaredb_params.directory:
|
|
# file_prefix = aaredb_params.directory
|
|
|
|
if (
|
|
getattr(aaredb_params, 'exposure', None) is None
|
|
and getattr(aaredb_params, 'transmission', None) is None
|
|
and getattr(aaredb_params, 'oscillation', None) is None
|
|
and getattr(aaredb_params, 'totalrange', None) is None
|
|
and getattr(aaredb_params, 'targetresolution', None) is None
|
|
):
|
|
return None, file_prefix
|
|
|
|
params = SimpleScanParameters()
|
|
|
|
if (exp := getattr(aaredb_params, 'exposure', None)) is not None:
|
|
params.exp_time_s = exp
|
|
|
|
if (trans := getattr(aaredb_params, 'transmission', None)) is not None:
|
|
logger.debug(f"transmission: {trans}")
|
|
params.transmission = trans / 100.0 if trans > 1.0 else trans
|
|
|
|
if (res := getattr(aaredb_params, 'targetresolution', None)) is not None:
|
|
logger.debug(f"resolution: {res}")
|
|
logger.debug(f"requested dtz: {self.diffraction_geometry.calc_dtz_mm(res)} ")
|
|
new_res = 1 / ((1 / res) + 0.1)
|
|
corrected_dtz = self.diffraction_geometry.calc_dtz_mm(new_res)
|
|
logger.debug(f"corrected dtz: {corrected_dtz}")
|
|
if corrected_dtz < 108:
|
|
corrected_dtz = 108
|
|
params.dtz = round(corrected_dtz)
|
|
|
|
osc = getattr(aaredb_params, 'oscillation', None)
|
|
total = getattr(aaredb_params, 'totalrange', None)
|
|
|
|
if osc is not None:
|
|
osc = abs(osc)
|
|
if osc > 0:
|
|
params.incr_omega_deg = osc
|
|
if total is not None and abs(total) > 0:
|
|
params.steps = round(abs(total) / osc)
|
|
else:
|
|
params.steps = round(360.0 / osc)
|
|
elif total is not None and abs(total) > 0:
|
|
default_osc = SimpleScanParameters().incr_omega_deg
|
|
params.incr_omega_deg = default_osc
|
|
params.steps = round(abs(total) / default_osc)
|
|
|
|
#if file_prefix is not None:
|
|
# params.file_prefix = file_prefix
|
|
|
|
return params, file_prefix
|
|
|
|
|
|
@property
|
|
def omega(self) -> float:
|
|
return self.__devs.aerotech.omega
|
|
|
|
@omega.setter
|
|
def omega(self, val: float):
|
|
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
|
self.__saved_box = None
|
|
if -720 < val < 720:
|
|
self.__devs.aerotech.move(val, wait=True, speed=180.0, direct=True)
|
|
self.__cfg.state_busy = False
|
|
else:
|
|
self.__cfg.state_busy = False
|
|
logger.error("Omega has to be between -720 and 720 degrees")
|
|
raise ValueError("Omega has to be between -720 and 720 degrees (for now)")
|
|
|
|
@property
|
|
def zoom(self) -> float:
|
|
return self.__devs.zoom
|
|
|
|
@zoom.setter
|
|
def zoom(self, val: float):
|
|
self.__saved_box = None
|
|
self.__devs.zoom = val
|
|
print(f"{self.__cfg.state}")
|
|
if self.__cfg.state == BeamlineStateEnum.BeamLocation:
|
|
self.__cfg.zoom_mode = ZoomModeEnum.BeamLocation
|
|
else:
|
|
self.__cfg.zoom_mode = ZoomModeEnum.User
|
|
zoom_settings = self.__cfg.zoom_settings.get_camera_settings(val)
|
|
print(f"Setting zoom {val} {zoom_settings}")
|
|
self.samcam_settings = zoom_settings
|
|
#elf.__cfg.state_busy = False
|
|
|
|
@property
|
|
def light(self) -> float:
|
|
val = self.__devs.lamp_light
|
|
if val <= 1.0:
|
|
return 0
|
|
elif val >= 2.5:
|
|
return 100.0
|
|
else:
|
|
return (val - 1.0) / 1.5 * 100.0
|
|
|
|
@light.setter
|
|
def light(self, f: float):
|
|
conv = (f / 100.0 * 1.5) + 1.0
|
|
print(f"Light {f} -> {conv}")
|
|
self.__devs.lamp_light = conv
|
|
|
|
@property
|
|
def sample(self) -> SampleShortInfo | None:
|
|
if self.__cfg.current_sample is not None and self.__cfg.current_sample.location is None:
|
|
return self.__cfg.current_sample
|
|
if self.__devs.tell.get_mounted_sample() is None:
|
|
return None
|
|
return self.__cfg.current_sample
|
|
|
|
@property
|
|
def samcam_settings(self) -> SampleCameraSettings:
|
|
return self.__devs.samcam_settings
|
|
|
|
@samcam_settings.setter
|
|
def samcam_settings(self, s: SampleCameraSettings):
|
|
self.__devs.samcam_settings = s
|
|
|
|
def autofocus(self, f: AutofocusSettings):
|
|
pos = self.__cfg.abr_meas_pos
|
|
|
|
z_min = pos.z - f.z_range_um / 2.0
|
|
z_max = pos.z + f.z_range_um / 2.0
|
|
|
|
measures = []
|
|
|
|
for i in range(f.z_steps):
|
|
pos.z = z_min + i * (z_max - z_min) / f.z_steps
|
|
self.__devs.abr_pos = pos
|
|
self.__devs.sample_cam.get_single_image()
|
|
image = self.__devs.sample_cam.get_image(gray=True)
|
|
val = calculate_focus_measure(image, f.center_x_pxl, f.center_y_pxl, f.radius_pxl)
|
|
measures.append((pos.z, val))
|
|
best_pos, _ = max(measures, key=lambda x: x[1])
|
|
|
|
pos.z = best_pos
|
|
self.__devs.abr_pos = pos
|
|
self.__cfg.abr_meas_pos = pos
|
|
self.__devs.sample_cam.collect_auto()
|
|
|
|
def tweak_abr_meas_pos(self, c: Coordinate):
|
|
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
|
try:
|
|
new_meas_pos = self.__cfg.abr_meas_pos + c
|
|
self.__cfg.abr_meas_pos = new_meas_pos
|
|
self.__devs.abr_pos = new_meas_pos
|
|
self.__saved_box = None
|
|
self.__cfg.state_busy = False
|
|
except:
|
|
self.__cfg.state_busy = False
|
|
raise
|
|
|
|
def save_abr_meas_pos(self):
|
|
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
|
try:
|
|
self.__cfg.abr_meas_pos = self.__devs.abr_pos
|
|
self.__cfg.state_busy = False
|
|
except:
|
|
self.__cfg.state_busy = False
|
|
raise
|
|
|
|
def goto_abr_meas_pos(self):
|
|
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
|
try:
|
|
self.__devs.abr_pos = self.__cfg.abr_meas_pos
|
|
self.__cfg.state_busy = False
|
|
except:
|
|
self.__cfg.state_busy = False
|
|
raise
|
|
|
|
def create_sample(self, target: SampleShortInfo):
|
|
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
|
|
try:
|
|
curr_sample = self.__cfg.current_sample
|
|
|
|
if curr_sample is not None and curr_sample.location is not None:
|
|
raise Exception("Sample from TELL is loaded")
|
|
|
|
self.__aare.create_manual_sample(target)
|
|
self.__cfg.current_sample = target
|
|
self.__cfg.state_busy = False
|
|
except:
|
|
self.__cfg.state_busy = False
|
|
raise
|
|
|
|
|
|
def __mount(self, target: SampleShortInfo | None):
|
|
self.__set_state(BeamlineStateEnum.RobotSampleExchange)
|
|
self.__saved_box = None
|
|
logger.info("Moving smargon to home")
|
|
self.__devs.smargon.move_home(wait=True)
|
|
logger.info("moving aerotech to mount position")
|
|
self.__devs.abr_pos = ABR_POS_MOUNT
|
|
time.sleep(0.1)
|
|
logger.info("checking beamstop")
|
|
if self.__devs.bsz.value < 24.0:
|
|
raise Exception("Beamstop Z below 24.0 mm - potentially unsafe with mounting")
|
|
logger.info(f"Checking magnet postion sensor positon: {self.__devs.magnet_position_sensor_readout.value}")
|
|
logger.info(f"Checking smargon position: {self.__devs.smargon.readback}")
|
|
logger.info(f"Checking abr position: {self.__devs.abr_pos}")
|
|
if self.__devs.magnet_position_sensor.value != 0:
|
|
time.sleep(1)
|
|
logger.warning("!!!!!!!!!!!!!!!!!!!!!MAGNET CONTROLLER BROKE AGAIN!!!!!!!!!!!!!!!!!!")
|
|
logger.debug(f"Checking magnet position sensor positon: {self.__devs.magnet_position_sensor_readout.value}")
|
|
logger.debug(f"Checking smargon position: {self.__devs.smargon.readback}")
|
|
logger.debug(f"Checking abr position: {self.__devs.abr_pos}")
|
|
if self.__devs.magnet_position_sensor.value != 0:
|
|
start = time.time()
|
|
end = start + 360
|
|
while self.__devs.magnet_position_sensor.value != 0:
|
|
time.sleep(1)
|
|
logger.debug(f"Checking magnet position sensor positon: {self.__devs.magnet_position_sensor_readout.value}")
|
|
logger.debug(f"Checking smargon position: {self.__devs.smargon.readback}")
|
|
logger.debug(f"Checking abr position: {self.__devs.abr_pos}")
|
|
if time.time() > end:
|
|
raise Exception(f"Magnet position sensor is not in position: {self.__devs.magnet_position_sensor_readout.value}")
|
|
|
|
#Reenable TELL after doors locked
|
|
logger.info("enable tell motion after doors locked")
|
|
self.__devs.tell.check_enable_motion()
|
|
|
|
self.__devs.tell.wait_mount_complete()
|
|
|
|
logger.debug("Moving tell to mount position")
|
|
self.__devs.tell.set_in_mount_position(True)
|
|
curr_sample = self.__cfg.current_sample
|
|
logger.info("Waiting for tell to be ready")
|
|
self.__devs.tell.wait_ready()
|
|
if curr_sample is not None and curr_sample.location is None:
|
|
logger.info(f"Unmounting current sample")
|
|
# TODO: Smarter way to know manual sample has be removed from goniometer
|
|
self.__aare.sample_unmounted(curr_sample)
|
|
self.__cfg.current_sample = None
|
|
elif target is None:
|
|
logger.info(f"Moving tell to unmount with no target")
|
|
self.__devs.tell.unmount(wait=True, timeout=360)
|
|
self.__aare.sample_unmounted(curr_sample)
|
|
self.__cfg.current_sample = None
|
|
else:
|
|
# reset zoom
|
|
self.zoom = 1
|
|
# mount sample
|
|
self.__aare.sample_unmounted(curr_sample)
|
|
logger.info(f"Moving tell to mount")
|
|
value = self.__devs.tell.mount(
|
|
address=target.tell_address(),
|
|
force=True,
|
|
auto_unmount=True,
|
|
read_dm=False,
|
|
wait=True,
|
|
timeout=360,
|
|
)
|
|
logger.debug(f"Post tell mount, pre db input")
|
|
if value is not None:
|
|
if value == "No Pin in Gripper":
|
|
logger.error("No sample was detected in gripper")
|
|
self.__devs.tell.dry(wait=True)
|
|
self.__aare.sample_failed(target, failed_comment = "No sample was detected in gripper")
|
|
raise Exception("No sample was detected in gripper, drying gripper")
|
|
elif value == "dry":
|
|
logger.info("Robot is drying")
|
|
else:
|
|
logger.info(f"Event detected from robot: {value}")
|
|
|
|
mounted = self.__devs.tell.get_mounted_sample()
|
|
logger.info(f"response from tell {mounted}")
|
|
if not mounted:
|
|
logger.error(f"Failed to mount target: {target.db_id} {target.location} {target.pin}")
|
|
self.__aare.sample_failed(target, failed_comment = "No sample was mounted")
|
|
raise Exception("No sample was mounted")
|
|
|
|
if target is not None:
|
|
self.__aare.sample_mounted(target)
|
|
logger.info(f"Target mounted: {target.db_id} {target.location} {target.pin}")
|
|
self.save_screenshot_db(target.db_id, f"sample_mounted")
|
|
else:
|
|
logger.info(f"Target is None: {target}")
|
|
self.__cfg.current_sample = target
|
|
|
|
|
|
# TODO: Need to know if dry needs to happen
|
|
|
|
@sample.setter
|
|
def sample(self, target: SampleShortInfo | None):
|
|
# This will set internally state to SampleExchange, but will return to SampleAlignment
|
|
# before exiting
|
|
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
|
|
try:
|
|
curr_sample = self.__cfg.current_sample
|
|
curr_sample_is_manual = False
|
|
logger.info(f"current sample: {curr_sample}")
|
|
logger.info(f"new target is: {target}")
|
|
self.__cfg.crystal_size = CrystalSize(x=0, y=0, z=0)
|
|
self.__cfg.last_best_b_factor = None
|
|
self.__cfg.last_best_res = None
|
|
self.__cfg.xrf = None
|
|
|
|
if curr_sample is not None and curr_sample.location is None:
|
|
self.__cfg.current_sample = None
|
|
curr_sample_is_manual = True
|
|
|
|
if target is not None or not curr_sample_is_manual:
|
|
self.__mount(target)
|
|
if target is not None and target.db_id is not None:
|
|
#self.__aare.sample_mounted(self.__cfg.current_sample)
|
|
logger.info(f"post_mount_{target.db_id}_{self.__devs.zoom}")
|
|
self.save_screenshot_db(target.db_id , f"post_mount_{target.db_id }_{self.__devs.zoom}")
|
|
except Exception as e:
|
|
self.__cfg.state_busy = False
|
|
# If unmount succeeded, but mount failed
|
|
mounted_sample = self.__devs.tell.get_mounted_sample()
|
|
#TODO check the following error logic
|
|
# if target is not None:
|
|
# print(f"TELL: Error mounting, TELL believes current sample is {mounted_sample}, while expected {target.location}")
|
|
if mounted_sample is None:
|
|
logger.error("TELL: After mounting there is no sample on gonio according to TELL")
|
|
self.__cfg.current_sample = None
|
|
if target is not None:
|
|
raise e
|
|
elif target is not None and (target.location is not None
|
|
and mounted_sample.puck.pos == target.location.pos
|
|
and mounted_sample.puck.segment == target.location.segment
|
|
and mounted_sample.pin == target.pin):
|
|
logger.info("TELL: Actually mounting was correct according to TELL, so setting current sample to target")
|
|
self.__cfg.current_sample = target
|
|
else:
|
|
raise e
|
|
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
|
|
#if target is not None and target.location is not None:
|
|
# self.__loop_center_sequence(target.db_id)
|
|
|
|
@property
|
|
def camera_image(self) -> np.ndarray | None:
|
|
image = self.__devs.sample_cam.get_image(gray=False)
|
|
return image
|
|
|
|
@property
|
|
def camera_image_gray(self) -> np.ndarray | None:
|
|
image = self.__devs.sample_cam.get_image(gray=True)
|
|
return image
|
|
|
|
def list_loaded_pucks(self) -> List[PuckLoadedInfo]:
|
|
return self.__devs.tell.get_detected_pucks()
|
|
|
|
|
|
def __auto_center(self, grid: RasterGridRequest) -> CompletedRasterGrid | None:
|
|
sample = self.sample
|
|
|
|
if sample is None:
|
|
raise Exception("Sample must be mounted to auto center")
|
|
|
|
old_prefix = grid.file_prefix
|
|
geom = self.sample_geometry
|
|
r = self.__ml_bounding_box(sample.db_id, f"ml_{geom.omega_deg:.2f}deg")
|
|
|
|
if r is None:
|
|
self.__devs.aerotech.move(geom.omega_deg + 90.0, wait=True)
|
|
time.sleep(0.2)
|
|
r = self.__ml_bounding_box(sample.db_id, f"ml_{geom.omega_deg + 90.0:.2f}deg")
|
|
|
|
if r is not None:
|
|
geom = self.sample_geometry
|
|
grid.smargon_top_left = r.smargon_top_left
|
|
grid.grid_size_mm = r.grid_size_mm
|
|
grid.n_x = r.n_x
|
|
grid.n_y = r.n_y
|
|
grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg"
|
|
grid.omega_deg = geom.omega_deg
|
|
res1 = self.__raster(grid)
|
|
self.__devs.reflector_up = True
|
|
time.sleep(0.1)
|
|
self.save_screenshot_db(sample.db_id, f"post_raster_{grid.omega_deg}deg")
|
|
time.sleep(0.1)
|
|
self.__devs.reflector_up = False
|
|
grid.omega_deg += 90
|
|
self.__devs.aerotech.move(grid.omega_deg, wait=True)
|
|
|
|
grid.n_x = 1
|
|
#TODO generate y scan rather than had code for 1x50
|
|
grid.n_y = 50
|
|
grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg"
|
|
grid.grid_size_mm = Coordinate(x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25)
|
|
offset = Coordinate(x=-grid.grid_size_mm.x/2.0, y=-(grid.n_y + 0.5) * grid.grid_size_mm.y / 2.0)
|
|
geom = self.sample_geometry
|
|
logger.debug(f"set offset y scan {offset}")
|
|
# self.__devs.smargon.target = geom.translate_smargon(offset)
|
|
grid.smargon_top_left = geom.translate_smargon(offset)
|
|
res2 = self.__raster(grid)
|
|
self.__devs.reflector_up = True
|
|
time.sleep(0.1)
|
|
self.save_screenshot_db(sample.db_id, f"post_raster_{grid.omega_deg}deg")
|
|
time.sleep(0.1)
|
|
self.__devs.reflector_up = False
|
|
return CompletedRasterGrid(r=[res1, res2])
|
|
else:
|
|
return None
|
|
|
|
|
|
def __raster(self, r: RasterGridRequest) -> CompletedRasterGridElem:
|
|
max_time = r.exp_time_s * r.n_y * r.n_x + 60
|
|
|
|
if r.dtz is not None:
|
|
self.__cfg.dtz = r.dtz
|
|
|
|
self.__set_state(BeamlineStateEnum.DataCollection)
|
|
|
|
save_smargon_position = self.__devs.smargon.readback
|
|
if r.smargon_top_left is not None:
|
|
delta_mm = self.sample_geometry.smargon_nudge(Coordinate(x=r.grid_size_mm.x / 2, y=r.grid_size_mm.y / 2))
|
|
|
|
self.__devs.smargon.target = SmargonCoordinate(sh_mm=r.smargon_top_left.sh_mm + delta_mm,
|
|
phi_deg=r.smargon_top_left.phi_deg,
|
|
chi_deg=r.smargon_top_left.chi_deg)
|
|
if r.transmission is not None:
|
|
self.__devs.transmission.set(r.transmission, wait=False)
|
|
self.__devs.aerotech.move(r.omega_deg, wait=True, speed=360.0)
|
|
self.__devs.transmission.wait()
|
|
self.__devs.smargon.wait()
|
|
|
|
status = self.status
|
|
logger.info(f"raster status: {status}")
|
|
logger.info(f"raster grid request: {r}")
|
|
self.__jfjoch.measure_raster(r, status)
|
|
self.__aare.create_gridscan_run(self.sample, r, status)
|
|
|
|
if self.sample is not None and self.sample.db_id is not None:
|
|
self.save_screenshot_db(self.sample.db_id, "before_raster")
|
|
|
|
if r.n_x == 1:
|
|
self.__devs.aerotech.measure_vertical_line(r.exp_time_s, r.grid_size_mm.y, r.n_y)
|
|
else:
|
|
self.__devs.aerotech.measure_raster_simple(
|
|
r.exp_time_s, r.grid_size_mm.x, r.grid_size_mm.y, r.n_x, r.n_y
|
|
)
|
|
self.__devs.aerotech.wait_scan_done(max_time)
|
|
self.__devs.aerotech.reset()
|
|
self.__devs.abr_pos = self.__cfg.abr_meas_pos
|
|
result = self.__jfjoch.wait_till_done(60)
|
|
|
|
images = result.images
|
|
|
|
result_array = create_quality_filtered_array(images, 'spots_low_res', min_spots=None,
|
|
min_efficiency=1.0, min_background=None, min_low_res_spots=None)
|
|
self.__cfg.crystal_size = get_xtal_size(self.__cfg.crystal_size, result_array, r)
|
|
com = raster_centre_of_mass(result_array, r)
|
|
if com is None:
|
|
logger.debug(f"using old method as COM is disabled")
|
|
com = identify_crystal_raster(result, r)
|
|
if com:
|
|
com_mm = com.get_com_mm(r)
|
|
grid_mm_x = com_mm.x
|
|
grid_mm_y = com_mm.y
|
|
else:
|
|
grid_mm_x = None
|
|
grid_mm_y = None
|
|
|
|
if grid_mm_x or grid_mm_y:
|
|
|
|
new_delta_mm = self.sample_geometry.smargon_nudge(Coordinate(x=grid_mm_x, y=grid_mm_y))
|
|
|
|
if r.n_x != 1:
|
|
result_list = get_result_list_from_com(images, com)
|
|
self.__cfg.last_best_b_factor = get_best_b_factor(result_list)
|
|
self.__cfg.last_best_res = get_best_res(result_list)
|
|
logger.debug(f"b_factor: {self.__cfg.last_best_b_factor}, best_res: {self.__cfg.last_best_res}")
|
|
|
|
else:
|
|
new_delta_mm = None
|
|
|
|
if new_delta_mm is not None:
|
|
if r.smargon_top_left:
|
|
new_target = r.smargon_top_left
|
|
else:
|
|
new_target = save_smargon_position
|
|
|
|
logger.debug(f'moving SMARGON to target new delta mm {new_target.sh_mm + new_delta_mm} mm')
|
|
self.__devs.smargon.target = SmargonCoordinate(sh_mm=new_target.sh_mm + new_delta_mm,
|
|
phi_deg=new_target.phi_deg,
|
|
chi_deg=new_target.chi_deg)
|
|
else:
|
|
logger.error("Auto finding optimal image failed due to no images found. Using previous position.")
|
|
self.__devs.smargon.target = save_smargon_position
|
|
|
|
if self.sample is not None and self.sample.db_id is not None and result is not None:
|
|
try:
|
|
self.__aare.ingest_gridscan(sample = self.sample, raster_result =result,
|
|
raster_request = r, geom = self.sample_geometry,
|
|
com = com)
|
|
except Exception as e:
|
|
logger.error(f"Exception ingesting grid scan: {e}")
|
|
|
|
return CompletedRasterGridElem(request=copy.deepcopy(r), result=result)
|
|
|
|
def measure_raster(self, r: RasterGridRequest, auto: bool) -> CompletedRasterGrid:
|
|
self.__cfg.try_set_busy(timeout=ceil(360))
|
|
try:
|
|
if auto:
|
|
result = self.__auto_center(r)
|
|
else:
|
|
raster_result = self.__raster(r)
|
|
result = CompletedRasterGrid(r=[raster_result])
|
|
self.__devs.reflector_up = True
|
|
time.sleep(0.1)
|
|
if self.sample is not None and self.sample.db_id is not None:
|
|
self.save_screenshot_db(self.sample.db_id, f"post_raster_{r.omega_deg}deg")
|
|
time.sleep(0.1)
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
return result
|
|
except Exception as e:
|
|
try:
|
|
self.__aare.axc_failed(self.sample)
|
|
except Exception as axc_e:
|
|
logger.error(f"Exception while reporting AXC failure: {axc_e}")
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
|
|
def __rotation(self, request: RotationScanRequest) -> CompletedRotationScan:
|
|
if request.dtz is not None:
|
|
logger.info(f'requesting dtz to move to {request.dtz}')
|
|
self.__cfg.dtz = request.dtz
|
|
if self.sample is not None and self.sample.db_id is not None:
|
|
self.save_screenshot_db(self.sample.db_id, f"pre_rotation{self.sample.db_id}_{self.__devs.zoom}")
|
|
self.__set_state(BeamlineStateEnum.DataCollection)
|
|
|
|
if request.transmission is not None:
|
|
logger.info(f'requesting transmission to move to {request.transmission}')
|
|
self.__devs.transmission.set(request.transmission, wait=False)
|
|
|
|
if request.start is not None:
|
|
self.__devs.smargon.target = request.start
|
|
|
|
self.__devs.transmission.wait()
|
|
if request.start is not None:
|
|
self.__devs.smargon.wait()
|
|
|
|
status = self.status
|
|
self.__jfjoch.measure_rotation(request, status, self.__cfg.xrf)
|
|
self.__aare.create_rotation_run(self.sample, request, status)
|
|
|
|
if self.sample is not None and self.sample.db_id is not None:
|
|
self.save_screenshot_db(self.sample.db_id, "before_dc")
|
|
|
|
if request.screening:
|
|
self.__devs.aerotech.measure_screening(request.start_omega_deg,
|
|
request.wedge_omega_deg,
|
|
request.exp_time_s,
|
|
request.incr_omega_deg,
|
|
request.steps)
|
|
self.__devs.aerotech.wait_scan_done(request.exp_time_s * request.steps + 60)
|
|
else:
|
|
|
|
total_omega = request.incr_omega_deg * request.steps
|
|
total_time = request.exp_time_s * request.steps
|
|
|
|
self.__devs.aerotech.measure_standard(
|
|
request.start_omega_deg, total_omega, total_time
|
|
)
|
|
|
|
if request.start is not None and request.end is not None:
|
|
smargon_time_step = request.time_sec / float(request.steps)
|
|
pos_step = (request.end.sh_mm - request.start.sh_mm) * (1.0 / float(request.steps))
|
|
|
|
for i in range(request.steps):
|
|
self.__devs.smargon.target = SmargonCoordinate(
|
|
sh_mm=request.start.sh_mm + pos_step * i
|
|
)
|
|
time.sleep(smargon_time_step)
|
|
|
|
self.__devs.aerotech.wait_scan_done(total_time + 60)
|
|
|
|
self.__devs.aerotech.reset()
|
|
result = self.__jfjoch.wait_till_done(60)
|
|
self.__aare.sample_collected(self.sample)
|
|
|
|
if self.sample is not None and self.sample.db_id is not None:
|
|
self.save_screenshot_db(self.sample.db_id, "after_dc")
|
|
|
|
return CompletedRotationScan(request=copy.deepcopy(request), result=result)
|
|
|
|
def measure_rotation(self, request: RotationScanRequest) -> CompletedRotationScan:
|
|
total_time = request.exp_time_s * request.steps
|
|
self.__cfg.try_set_busy(timeout=ceil(total_time + 360))
|
|
|
|
try:
|
|
result = self.__rotation(request)
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
return result
|
|
except Exception as e:
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
|
|
def get_background(self):
|
|
#if self.sample is not None:
|
|
# raise Exception("Background cannot be measured when sample is mounted")
|
|
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
try:
|
|
self.__devs.lamp_light = 2.5
|
|
self.__cfg.zoom_mode = ZoomModeEnum.LoopCenter
|
|
zoom_settings = self.__cfg.zoom_settings.z
|
|
for zoom_value in zoom_settings:
|
|
exposure = zoom_settings[zoom_value].exposure
|
|
gain = zoom_settings[zoom_value].gain
|
|
self.__devs.samcam_settings = SampleCameraSettings(exposure=exposure, gain=gain)
|
|
self.__devs.zoom_sync(zoom_value)
|
|
time.sleep(5.0) # Wait for settings to stabilize
|
|
image = self.__devs.sample_cam.get_image(gray=False)
|
|
self.__cfg.put_alc_bkg(zoom_value, exposure, gain, image)
|
|
bgr_array = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
|
|
cv2.imwrite(f"bkg{zoom_value:.0f}_{exposure*1000:.0f}_{gain:.0f}.jpg", bgr_array)
|
|
self.__cfg.state_busy = False
|
|
self.__devs.sample_cam.set_auto()
|
|
except Exception as e:
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
|
|
@property
|
|
def dtz(self) -> float:
|
|
tmp = self.__cfg.dtz
|
|
if tmp is None:
|
|
return 150.0
|
|
else:
|
|
return tmp
|
|
|
|
@dtz.setter
|
|
def dtz(self, val: float):
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
state = self.__cfg.state
|
|
|
|
if val < self.__devs.dtz_low or val > self.__devs.dtz_high:
|
|
self.__cfg.state_busy = False
|
|
raise RuntimeError(f"dtz={val} outside limits {self.__devs.dtz_low} to {self.__devs.dtz_high}")
|
|
|
|
if state == BeamlineStateEnum.DataCollection:
|
|
self.__cfg.state_busy = False
|
|
raise RuntimeError("Cannot set dtz during data collection")
|
|
elif state == BeamlineStateEnum.SampleAlignment:
|
|
self.__devs.dtz.move(val, wait=False)
|
|
self.__cfg.dtz = val
|
|
self.__cfg.state_busy = False
|
|
|
|
@property
|
|
def smargon(self) -> SmargonCoordinate:
|
|
return self.__devs.smargon.readback
|
|
|
|
@smargon.setter
|
|
def smargon(self, sc: SmargonCoordinate):
|
|
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
|
try:
|
|
self.__saved_box = None
|
|
self.__devs.smargon.target = sc
|
|
self.__devs.smargon.wait()
|
|
self.__cfg.state_busy = False
|
|
except Exception as e:
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
pass
|
|
|
|
def mark_beam(self, x_pxl: float, y_pxl: float):
|
|
self.__cfg.set_busy(BeamlineStateEnum.BeamLocation)
|
|
try:
|
|
self.__cfg.mark_beam(x_pxl, y_pxl, self.__devs.zoom)
|
|
self.__cfg.state_busy = False
|
|
except Exception as e:
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
|
|
def clear_mark_beam(self):
|
|
self.__cfg.set_busy(BeamlineStateEnum.BeamLocation)
|
|
self.__cfg.clear_mark_beam()
|
|
self.__cfg.state_busy = False
|
|
|
|
@property
|
|
def sample_geometry(self) -> SampleGeometryModel:
|
|
zoom = self.__devs.zoom
|
|
abr_pos = self.__cfg.abr_meas_pos
|
|
try:
|
|
aerotech_coord = Coordinate(
|
|
x=self.__devs.gmx.value - abr_pos.x,
|
|
y=self.__devs.gmy.value - abr_pos.y,
|
|
z=self.__devs.gmz.value - abr_pos.z,
|
|
)
|
|
sample_geom = SampleGeometryModel(
|
|
beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom),
|
|
pixel_in_mm=self.__cfg.pixel_to_mm(zoom),
|
|
omega_deg=self.__devs.aerotech.omega,
|
|
smargon=self.__devs.smargon.readback,
|
|
beam_size_mm=self.__cfg.beam_size_mm,
|
|
aerotech=aerotech_coord,
|
|
aerotech_meas=self.__devs.abr_pos
|
|
)
|
|
return sample_geom
|
|
|
|
except Exception as e:
|
|
|
|
try:
|
|
test_x = self.__devs.gmx.value - abr_pos.x
|
|
print(f"Error creating sample geometry model: {e}")
|
|
print(f"Error not ABR, check smargon then restart server")
|
|
raise Exception(f"Error getting sample geometry {e}")
|
|
|
|
except:
|
|
print(f"Error getting sample geometry: {e}")
|
|
print(f"!!!!! ABR MAY HAVE DISCONNECTED !!!!!")
|
|
print(f"!!!!! CHECK ABR EPICS PANEL FOR ERRORS!!!!!")
|
|
print("!!!!! IF NOT RESTART GUI !!!!!")
|
|
print("CLOSING EXPERIMENTAL HUCTH FOR SAFETY")
|
|
self.__devs.exp_shutter.close()
|
|
return SampleGeometryModel(
|
|
beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom),
|
|
pixel_in_mm=self.__cfg.pixel_to_mm(zoom),
|
|
omega_deg=self.__devs.aerotech.omega,
|
|
smargon=self.__devs.smargon.readback,
|
|
beam_size_mm=self.__cfg.beam_size_mm,
|
|
aerotech=Coordinate(x=0.0, y=0.0, z=0.0),
|
|
aerotech_meas=self.__devs.abr_pos
|
|
)
|
|
|
|
@property
|
|
def beam_center(self) -> Tuple[float, float]:
|
|
return self.__cfg.beam_center
|
|
|
|
@beam_center.setter
|
|
def beam_center(self, val: Tuple[float, float]):
|
|
self.__cfg.beam_center = val
|
|
|
|
@property
|
|
def beam_size_mm(self) -> Coordinate:
|
|
return self.__cfg.beam_size_mm
|
|
|
|
@beam_size_mm.setter
|
|
def beam_size_mm(self, val: Coordinate):
|
|
self.__cfg.beam_size_mm = val
|
|
|
|
def get_beam_mark(self):
|
|
return self.__cfg.get_beam_mark(self.__devs.zoom)
|
|
|
|
def listen_changes(self) -> redis.client.PubSub:
|
|
return self.__cfg.listen_changes()
|
|
|
|
def __ml_bounding_box(self, sample_id: int | None = None, filename: str | None = None) -> RasterGridRequest | None:
|
|
time.sleep(0.2) # Just to be sure image is stable
|
|
#curr_image = self.camera_image
|
|
#box = self.__mlbox.predict(curr_image)
|
|
curr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
|
|
m = self.__mlbox.predict(curr_image, preferred_class=(3,0))
|
|
if m is None:
|
|
if filename is not None:
|
|
cv2.imwrite(f"{filename}_no_detection.jpg", curr_image)
|
|
self.__aare.upload_image(sample_id, f"{filename}_no_detection", curr_image)
|
|
return None
|
|
x1, y1, x2, y2 = m.box.top_x, m.box.top_y, m.box.bottom_x, m.box.bottom_y
|
|
|
|
if filename is not None:
|
|
cv2.rectangle(curr_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
|
|
cv2.imwrite(f"{filename}.jpg", curr_image)
|
|
self.__aare.upload_image(sample_id, filename, curr_image)
|
|
|
|
geom = self.sample_geometry
|
|
|
|
start_coord = geom.picture_to_smargon(Coordinate(x=x1, y=y1))
|
|
grid_size = Coordinate(x=geom.beam_size_mm.x * 0.8, y=geom.beam_size_mm.y * 0.8)
|
|
n_x = abs(ceil((x2 - x1) * geom.pixel_in_mm / grid_size.x))
|
|
n_y = abs(ceil((y2 - y1) * geom.pixel_in_mm / grid_size.y))
|
|
|
|
return RasterGridRequest(
|
|
exp_time_s=0.02,
|
|
transmission=1.0,
|
|
smargon_top_left = SmargonCoordinate(chi_deg = geom.smargon.chi_deg,
|
|
phi_deg= geom.smargon.phi_deg,
|
|
sh_mm=start_coord),
|
|
n_x=n_x,
|
|
n_y=n_y,
|
|
grid_size_mm= grid_size,
|
|
omega_deg=geom.omega_deg
|
|
)
|
|
|
|
def __ml_loop_centre_box(self, sample_id: int | None = None, filename: str | None = None) -> tuple[SmargonCoordinate | None, int| None, list[int] | None]:
|
|
#time.sleep(0.2) # Just to be sure image is stable
|
|
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
|
|
boxes = self.__mlbox.predict_all_best(bgr_image, overlap_with_pin = 0.5, confidence_min=0.3)
|
|
|
|
if boxes is None:
|
|
if filename is not None:
|
|
self.__aare.upload_image(sample_id, f"{filename}_no_detection", bgr_image)
|
|
return None, None, None
|
|
|
|
classes: list[int] = []
|
|
pin = None
|
|
centre_x, centre_y = None, None
|
|
for box in boxes.boxes.values():
|
|
if box and box.cls is not None:
|
|
classes.append(int(box.cls.value))
|
|
if int(box.cls.value) == 1:
|
|
pin=box
|
|
|
|
best_box = self.__mlbox.get_preferred_class_box(boxes, (2,3,0,1))
|
|
if best_box is None or best_box.box is None or best_box.cls is None:
|
|
return None, None, classes if classes else None
|
|
|
|
|
|
cls = int(best_box.cls.value)
|
|
x1 = best_box.box.top_x
|
|
y1 = best_box.box.top_y
|
|
x2 = best_box.box.bottom_x
|
|
y2 = best_box.box.bottom_y
|
|
|
|
if filename is not None:
|
|
self.__aare.upload_image(sample_id, filename, bgr_image)
|
|
|
|
geom = self.sample_geometry
|
|
|
|
if cls == 0: # loop_all
|
|
if y1 + y2 <= x1 + x2:
|
|
centre_y = y1 + (y2 - y1) / 2
|
|
centre_x = x1
|
|
elif pin:
|
|
position_dict = self.__mlbox.check_box_relation(pin, best_box)
|
|
if position_dict["overlap_y"] and position_dict["overlap_x"]:
|
|
centre_y = y1 + (y2 - y1) / 2
|
|
centre_x = x1
|
|
cls = 1
|
|
logger.debug("significant overlap between pin and loop_all not picked up by ML")
|
|
else:
|
|
centre_y = y2 if position_dict["top"] else y1
|
|
centre_x = x1 + (x2 - x1) / 2
|
|
else:
|
|
centre_y = y1
|
|
centre_x = x1 + (x2 - x1) / 2
|
|
|
|
elif cls == 1: # pin
|
|
centre_y = y1 + (y2 - y1)/2
|
|
centre_x = x1
|
|
|
|
elif cls == 2 or cls == 3: #crystal or loop_face
|
|
centre_y = y1 + (y2 - y1)/2
|
|
centre_x = x1 + (x2 - x1)/2
|
|
|
|
else:
|
|
logger.debug(f"unknown box class {cls}")
|
|
return None, cls, classes if classes else None
|
|
|
|
coord = geom.picture_to_smargon(Coordinate(x=centre_x, y=centre_y))
|
|
return SmargonCoordinate(sh_mm=coord), cls, classes
|
|
|
|
def ml_bounding_box(self, sample_id: int | None = None, filename: str | None = None) -> RasterGridRequest | None:
|
|
try:
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
r = self.__ml_bounding_box(sample_id, filename)
|
|
self.__cfg.state_busy = False
|
|
return r
|
|
except Exception:
|
|
self.__cfg.state_busy = False
|
|
raise
|
|
|
|
def face_detection(self, steps: int = 14, step_size: int = 15) -> dict:
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
try:
|
|
logger.info("running face detection sequence")
|
|
result = self.__face_detection_sequence(steps=steps, step_size=step_size)
|
|
except Exception as e:
|
|
logger.error(f"error in face detection sequence {e}")
|
|
result = {
|
|
"samples": None,
|
|
"height_fit": None,
|
|
"area_fit": None,
|
|
}
|
|
self.__cfg.state_busy = False
|
|
return result
|
|
|
|
def face_detection_centre_correction(self, m:MLBoxModel, tolerance: float = 0.2):
|
|
geom = self.sample_geometry
|
|
beam_y = geom.beam_location_pxl.y
|
|
beam_x = geom.beam_location_pxl.x
|
|
|
|
x1 = m.box.top_x
|
|
y1 = m.box.top_y
|
|
y2 = m.box.bottom_y
|
|
|
|
centre_y = y1 + (y2 - y1) / 2
|
|
centre_x = x1
|
|
|
|
if beam_y !=0 and abs(centre_y-beam_y)/abs(beam_y) > tolerance:
|
|
coord = geom.picture_to_smargon(Coordinate(x=beam_x, y=centre_y))
|
|
self.__devs.smargon.target = SmargonCoordinate(sh_mm=coord)
|
|
self.__devs.smargon.wait(60)
|
|
|
|
return
|
|
|
|
def __face_detection_sequence(self, steps: int = 14, step_size: int = 15) -> dict:
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__devs.lamp_light = 2.5
|
|
self.__cfg.zoom_mode = ZoomModeEnum.LoopCenter
|
|
|
|
zoom_settings = self.__cfg.zoom_settings.z
|
|
zoom_value = self.__devs.zoom
|
|
logger.info('face detection sequence')
|
|
|
|
self.__devs.samcam_settings = SampleCameraSettings(
|
|
exposure=zoom_settings[zoom_value].exposure,
|
|
gain=zoom_settings[zoom_value].gain)
|
|
self.__devs.zoom_sync(zoom_value)
|
|
|
|
boxes: dict[int, tuple[float, float, float, float]] = {}
|
|
curr_angle = int(self.__devs.aerotech.omega)
|
|
total_range = steps * step_size + 1
|
|
start_angle = curr_angle if curr_angle + total_range < 720 else 0
|
|
end_angle = curr_angle + total_range
|
|
|
|
for angle in range(start_angle, end_angle, step_size):
|
|
logger.debug(f'moving to angle: {angle}')
|
|
rotate_time = time.perf_counter()
|
|
self.__devs.aerotech.move(angle, wait=True)
|
|
logger.info(f"time to rotate 15 degrees: {time.perf_counter() - rotate_time}")
|
|
|
|
curr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
|
|
box_time = time.perf_counter()
|
|
m = self.__mlbox.predict(curr_image, filename=None, preferred_class = (3,0))
|
|
logger.info(f"time to predict: {time.perf_counter() - box_time}")
|
|
|
|
if not m or not m.box:
|
|
logger.info(f"no box found for angle {angle}")
|
|
continue
|
|
|
|
cls_id = int(m.cls.value)
|
|
x1, y1, x2, y2 = m.box.top_x, m.box.top_y, m.box.bottom_x, m.box.bottom_y
|
|
|
|
self.face_detection_centre_correction(m, tolerance=0.2)
|
|
|
|
if cls_id == 3:
|
|
boxes[angle] = (x1, y1, x2, y2)
|
|
logger.info(f"accepted box at angle {angle}, cls={cls_id}, box={(x1, y1, x2, y2)}")
|
|
else:
|
|
logger.debug(f"ignoring class {cls_id} (pin/crystal) at angle {angle}")
|
|
|
|
if not boxes:
|
|
logger.info("no boxes found")
|
|
return {"samples": None, "height_fit": None, "area_fit": None}
|
|
|
|
best_fit_angle_area, area_params = fd.get_flat_face(boxes, start_angle, end_angle, True)
|
|
best_fit_angle_height, height_params = fd.get_flat_face(boxes, start_angle, end_angle, False)
|
|
fit_results = {"Area":{"angle":best_fit_angle_area, "params":area_params}, "Height": {"angle":best_fit_angle_height, "params":height_params}}
|
|
logger.info(f"best angle by area: {best_fit_angle_area}")
|
|
logger.info(f"best angle by height: {best_fit_angle_height}")
|
|
flat_face_angle, best_params, best_name = fd.choose_best_fit(fit_results)
|
|
logger.info(f"best params: {best_params}")
|
|
logger.info(f"chosen fit: {best_name}")
|
|
logger.info(f"best angle: {flat_face_angle}")
|
|
self.__devs.aerotech.move(flat_face_angle, wait=True)
|
|
|
|
samples_out = fd.get_samples_out(boxes)
|
|
logger.info(f"face detection sequence done, samples: {samples_out}")
|
|
|
|
return {
|
|
"samples": samples_out,
|
|
"height_fit": {"A": height_params["A"],
|
|
"B": height_params["B"],
|
|
"phi_rad": height_params["phi_rad"],
|
|
"C": height_params["C"],
|
|
"best_angle_deg": best_fit_angle_height
|
|
},
|
|
"area_fit": {"A": area_params["A"],
|
|
"B": area_params["B"],
|
|
"phi_rad": area_params["phi_rad"],
|
|
"C": height_params["C"],
|
|
"best_angle_deg": best_fit_angle_area
|
|
},
|
|
}
|
|
|
|
|
|
def __loop_center_sequence(self, sample_id: int | None = None) -> bool:
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__devs.lamp_light = 2.5
|
|
|
|
found_classes_count: dict[int, int] = {0:0, 1:0, 2:0, 3:0}
|
|
|
|
try:
|
|
self.__cfg.zoom_mode = ZoomModeEnum.LoopCenter
|
|
zoom_settings = self.__cfg.zoom_settings.z
|
|
|
|
for zoom_iter, zoom_value in enumerate(zoom_settings):
|
|
exposure = zoom_settings[zoom_value].exposure
|
|
gain = zoom_settings[zoom_value].gain
|
|
max_attempt = 2
|
|
attempt = 0
|
|
|
|
base_angles = (0, 45, 90) if (zoom_iter % 2 == 0) else (90, 45, 0)
|
|
if sample_id is not None:
|
|
self.save_screenshot_db(sample_id, f"pre_alc")
|
|
|
|
while attempt < max_attempt:
|
|
self.__devs.samcam_settings = SampleCameraSettings(exposure=exposure, gain=gain)
|
|
exp = int(exposure * 1000)
|
|
gn = int(gain)
|
|
self.__devs.zoom_sync(zoom_value)
|
|
logger.debug(f'zoom={zoom_value},gain={gn}, exp={exp}ms')
|
|
|
|
found_flag = False
|
|
found_angle: int | None = None
|
|
targets_found_this_attempt = 0
|
|
|
|
for angle in base_angles:
|
|
logger.debug(f"Moving to new omega: {angle}")
|
|
time_to_move_aerotech= time.perf_counter()
|
|
self.__devs.aerotech.move(angle, wait=True)
|
|
logger.info(f"time to move: {time.perf_counter()-time_to_move_aerotech}")
|
|
|
|
filename = f"{sample_id}_{angle}_{zoom_value:.0f}_{exp}_{gn}" if sample_id is not None else None
|
|
|
|
try:
|
|
target, cls, classes = self.__ml_loop_centre_box(sample_id, filename)
|
|
except Exception as e:
|
|
logger.error(f"Error getting ML box for angle {angle}")
|
|
logger.error(f"Exception: {e}")
|
|
target, cls, classes = None, None, None
|
|
|
|
if target is None:
|
|
logger.debug("no target found")
|
|
continue
|
|
|
|
if classes:
|
|
for c in classes:
|
|
found_classes_count[int(c)] += 1
|
|
logger.debug(f"classes found: {classes}")
|
|
logger.debug(f"class found: {cls}")
|
|
if cls is not None and cls != 1:
|
|
targets_found_this_attempt += 1
|
|
found_flag = True
|
|
found_angle = angle
|
|
|
|
time_to_move_smargon = time.perf_counter()
|
|
self.__devs.smargon.target = target
|
|
self.__devs.smargon.wait(60)
|
|
logger.info(f"time to move smargon: {time.perf_counter() - time_to_move_smargon}")
|
|
|
|
if targets_found_this_attempt == 0:
|
|
logger.error(f"No targets found in this attempt {attempt}")
|
|
raise LoopCenteringFailed
|
|
|
|
else:
|
|
if targets_found_this_attempt >= len(base_angles):
|
|
logger.debug(f"sucessfully found {targets_found_this_attempt} targets in attempt {attempt + 1} ")
|
|
break
|
|
if found_flag is not None and found_angle is not None:
|
|
logger.debug(f"found a target at angle {found_angle} in attempt {attempt + 1}")
|
|
base_angles = (found_angle, found_angle + 45, found_angle + 90)
|
|
logger.debug(f"new base angles: {base_angles}")
|
|
attempt += 1
|
|
logger.debug(f"attempt {attempt} of {max_attempt}")
|
|
if attempt >= max_attempt:
|
|
logger.error(f"{attempt} exceeds max attempts {max_attempt}")
|
|
raise LoopCenteringFailed
|
|
|
|
#i += 1
|
|
logger.debug("alc success")
|
|
self.__aare.sample_centered(self.__cfg.current_sample)
|
|
if sample_id is not None:
|
|
self.save_screenshot_db(sample_id, f"{sample_id}_centered")
|
|
return True
|
|
|
|
except Exception as e:
|
|
total_detections = sum(found_classes_count.values())
|
|
if total_detections == 0:
|
|
logger.error("ALC: No objects found in any zoom")
|
|
self.__aare.alc_failed(self.sample, alc_comment="No objects detected")
|
|
else:
|
|
self.__aare.alc_failed(
|
|
self.sample,
|
|
alc_comment=(
|
|
"Failed to centre but detected objects - "
|
|
f"Crystal: {found_classes_count.get(2,0)}, "
|
|
f"Loop_face: {found_classes_count.get(3,0)}, "
|
|
f"Loop_all: {found_classes_count.get(0,0)}, "
|
|
f"Pin: {found_classes_count.get(1,0)}"
|
|
),
|
|
)
|
|
logger.error("Failed to centre but detected objects - "
|
|
f"Crystal: {found_classes_count.get(2,0)}, "
|
|
f"Loop_face: {found_classes_count.get(3,0)}, "
|
|
f"Loop_all: {found_classes_count.get(0,0)}, "
|
|
f"Pin: {found_classes_count.get(1,0)}")
|
|
logger.info(f"Error in loop centering: {e}")
|
|
return False
|
|
|
|
def auto_loop_center(self, sample_id: int | None = None) -> float:
|
|
start = time.perf_counter()
|
|
try:
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
if not self.__loop_center_sequence(sample_id):
|
|
raise LoopCenteringFailed
|
|
self.__cfg.state_busy = False
|
|
|
|
except Exception:
|
|
self.__cfg.zoom_mode = ZoomModeEnum.User
|
|
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
|
|
self.__cfg.state_busy = False
|
|
raise
|
|
|
|
self.__cfg.zoom_mode = ZoomModeEnum.User
|
|
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
|
|
|
|
end = time.perf_counter()
|
|
return end - start
|
|
|
|
def save_screenshot(self, filename: str):
|
|
#time.sleep(0.2) # Wait 200 ms to ensure camera image is stable
|
|
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
|
|
cv2.imwrite(f"{filename}.jpg", bgr_image)
|
|
|
|
def save_screenshot_db(self, sample_id: int, filename: str):
|
|
#time.sleep(0.2) # Wait 200 ms to ensure camera image is stable
|
|
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
|
|
self.__aare.upload_image(sample_id, filename, bgr_image)
|
|
|
|
@property
|
|
def sample_spreadsheet(self) -> SampleShortInfoList:
|
|
return self.__cfg.spreadsheet
|
|
|
|
@property
|
|
def reference_tools(self) -> SampleShortInfoList:
|
|
return self.__cfg.reference_tools
|
|
|
|
def sample_spreadsheet_user(self, pgroup: str) -> SampleShortInfoList:
|
|
sample = self.sample_spreadsheet
|
|
sample.s = list(filter(lambda x: x.user == pgroup, sample.s))
|
|
return sample
|
|
|
|
def get_auto_raster_params(self) -> SimpleScanParameters:
|
|
|
|
if self.status.sample is None:
|
|
return SimpleScanParameters(dtz=150, exp_time_s=0.04, transmission=1.0)
|
|
|
|
aaredb_params = self.status.sample.aaredb_params if hasattr(self.status.sample, "aaredb_params") else None
|
|
|
|
if aaredb_params is None:
|
|
return SimpleScanParameters(dtz=150, exp_time_s=0.04, transmission=1.0)
|
|
|
|
params = SimpleScanParameters()
|
|
|
|
# Exposure
|
|
exp = getattr(aaredb_params, 'exposure', None)
|
|
if exp is not None:
|
|
params.exp_time_s = exp
|
|
else:
|
|
params.exp_time_s = 0.04 # Default
|
|
|
|
# Transmission
|
|
trans = getattr(aaredb_params, 'transmission', None)
|
|
if trans is not None:
|
|
logger.debug(f"transmission: {trans}")
|
|
params.transmission = trans / 100.0 if trans > 1.0 else trans
|
|
else:
|
|
params.transmission = 1.0 # Default
|
|
|
|
# Resolution and DTZ
|
|
res = getattr(aaredb_params, 'targetresolution', None)
|
|
if res is not None:
|
|
logger.debug(f"resolution: {res}")
|
|
try:
|
|
params.dtz = self.diffraction_geometry.calc_dtz_mm(res)
|
|
logger.debug(f"dtz: {params.dtz}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to calculate dtz for resolution {res}: {e}")
|
|
params.dtz = 150 # Fallback default
|
|
|
|
# Adjust exposure time based on resolution
|
|
if res <= 1.5:
|
|
params.exp_time_s = 0.02
|
|
elif 1.5 < res <= 3.0:
|
|
params.exp_time_s = 0.04
|
|
else:
|
|
params.exp_time_s = 0.08
|
|
else:
|
|
params.dtz = 150
|
|
params.exp_time_s = 0.04
|
|
|
|
return params
|
|
|
|
def get_collection_params(self, prefer_smart: bool = False) -> tuple[SimpleScanParameters, str]:
|
|
spreadsheet_params, file_prefix = self.spreadsheet_params()
|
|
logger.debug(f"spreadsheet_params: {spreadsheet_params}")
|
|
smart_params = self.__cfg.auto_params
|
|
default_params = SimpleScanParameters(exp_time_s=0.04, dtz=110, incr_omega_deg=0.2)
|
|
#if file_prefix is not None:
|
|
# default_params.file_prefix = file_prefix
|
|
#self.__aare.send_msg_to_db(self.sample,event_type=SampleEventType(''), comment=f'smart_params: {smart_params}')
|
|
if prefer_smart:
|
|
if smart_params:
|
|
#if file_prefix:
|
|
# smart_params.file_prefix = file_prefix
|
|
return smart_params, "smart_params"
|
|
if spreadsheet_params:
|
|
return spreadsheet_params, "spreadsheet_params"
|
|
else:
|
|
if spreadsheet_params:
|
|
return spreadsheet_params, "spreadsheet_params"
|
|
if smart_params:
|
|
#if file_prefix:
|
|
# smart_params.file_prefix = file_prefix
|
|
return smart_params, "smart_params"
|
|
|
|
return default_params, "defaults"
|
|
|
|
|
|
def measure(self, sample: SampleShortInfo) -> float:
|
|
start = time.perf_counter()
|
|
formatted_date = datetime.now().strftime('%Y%m%d')
|
|
sample_prefix = "{}/{}/{:02d}/{}".format(
|
|
formatted_date,
|
|
sample.puck_name,
|
|
sample.pin,
|
|
sample.sample_name
|
|
)
|
|
|
|
try:
|
|
logger.info(f"setting busy at {time.perf_counter() - start}")
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
logger.info(f"set busy at {time.perf_counter() - start}")
|
|
geom = self.sample_geometry
|
|
start_mount=time.perf_counter()
|
|
logger.info(f"starting mount {sample.db_id} at {time.ctime()}")
|
|
self.__mount(sample)
|
|
logger.info(f"mounting done at {time.perf_counter() - start_mount}, total time: {time.perf_counter() - start}")
|
|
alc_time = time.perf_counter() - start
|
|
logger.info(f"starting alc at {alc_time}")
|
|
if not self.__loop_center_sequence(sample.db_id):
|
|
self.__aare.alc_failed(sample)
|
|
print("alc failed")
|
|
self.__cfg.state_busy = False
|
|
end = time.perf_counter()
|
|
return end - start
|
|
#raise LoopCenteringFailed
|
|
logger.info(f"alc done at {time.perf_counter() - start}")
|
|
self.__face_detection_sequence()
|
|
logger.info(f"face_detection done at {time.perf_counter() - start}")
|
|
self.zoom = 500
|
|
|
|
hex_string = secrets.token_hex(3) # 3 bytes = 6 hex characters
|
|
print(hex_string.upper())
|
|
|
|
raster_params = self.get_auto_raster_params()
|
|
if self.__auto_center(RasterGridRequest(
|
|
exp_time_s=raster_params.exp_time_s,
|
|
file_prefix=sample_prefix + f"_{hex_string}",
|
|
smargon_top_left= SmargonCoordinate(),
|
|
n_x=1,
|
|
n_y=1,
|
|
dtz=raster_params.dtz,
|
|
grid_size_mm=Coordinate(x=geom.beam_size_mm.x * 0.5, y=geom.beam_size_mm.y * 0.5),
|
|
omega_deg=self.omega,
|
|
transmission = raster_params.transmission,
|
|
)):
|
|
logger.info(f"raster scans done at {time.perf_counter() - start}")
|
|
params, source = self.get_collection_params(prefer_smart=False)
|
|
logger.info(f"Using {source} for data collection: {params}")
|
|
|
|
self.__cfg.zoom_mode = ZoomModeEnum.User
|
|
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
|
|
self.__devs.dtz.move(params.dtz, wait=True)
|
|
if self.omega + 180 < 720:
|
|
start_omega = self.omega
|
|
else:
|
|
start_omega = params.start_omega_deg
|
|
self.__rotation( RotationScanRequest(start_omega_deg=start_omega,
|
|
dtz=params.dtz,
|
|
file_prefix="data/" + sample_prefix + f"_{hex_string}",
|
|
exp_time_s=params.exp_time_s,
|
|
incr_omega_deg=params.incr_omega_deg,
|
|
steps=params.steps,
|
|
transmission=params.transmission,
|
|
))
|
|
logger.info(f"rotation done at {time.perf_counter() - start}")
|
|
else:
|
|
logger.error("auto center failed")
|
|
self.__aare.axc_failed(sample)
|
|
self.zoom = 1
|
|
self.__cfg.zoom_mode = ZoomModeEnum.User
|
|
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
|
|
self.__cfg.state_busy = False
|
|
except Exception as e:
|
|
logger.error(f"Error in measure: {e}")
|
|
self.__aare.sample_failed(sample)
|
|
self.__cfg.state_busy = False
|
|
end = time.perf_counter()
|
|
return end - start
|
|
#raise
|
|
|
|
end = time.perf_counter()
|
|
return end - start
|
|
|
|
def __set_state(self, target: BeamlineStateEnum):
|
|
# __set_state assumes that beamline is already in busy state
|
|
# it will apply a proper transformation and change state afterward
|
|
# specifically:
|
|
# 1. If target state is maintenance, just go there
|
|
# 2. If target state is same as current, nothing will happen
|
|
# 3. If target state cannot be reached, exception is raised and current state is kept
|
|
# 4. If exception is raised during transformation, state is set to maintenance
|
|
# 5. If transformation goes OK, target state is set
|
|
#
|
|
# Busy state will be cleared only, if exception is raised.
|
|
#
|
|
curr_state = self.__cfg.state
|
|
|
|
if not self.__cfg.state_busy:
|
|
raise Exception("Beamline should be busy")
|
|
|
|
if target == BeamlineStateEnum.Maintenance:
|
|
self.__cfg.state = BeamlineStateEnum.Maintenance
|
|
elif target != curr_state:
|
|
self.__cfg.state = BeamlineStateEnum.Moving
|
|
try:
|
|
match curr_state:
|
|
case BeamlineStateEnum.Maintenance:
|
|
if target == BeamlineStateEnum.SampleExchange:
|
|
workflows.m2se(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.SampleExchange:
|
|
if target == BeamlineStateEnum.SampleAlignment:
|
|
workflows.se2sa(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.BeamLocation:
|
|
workflows.se2sa(self.__devs, self.__cfg)
|
|
workflows.sa2bl(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.DewarTransfer:
|
|
if target == BeamlineStateEnum.SampleAlignment:
|
|
workflows.dh2sa(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.DataCollection:
|
|
if target == BeamlineStateEnum.SampleAlignment:
|
|
workflows.dc2sa(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.RobotSampleExchange:
|
|
workflows.dc2rse(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.BeamLocation:
|
|
if target == BeamlineStateEnum.SampleAlignment:
|
|
workflows.bl2sa(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.SampleExchange:
|
|
workflows.bl2sa(self.__devs, self.__cfg)
|
|
workflows.sa2se(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.SampleAlignment:
|
|
if target == BeamlineStateEnum.DewarTransfer:
|
|
workflows.sa2dh(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.SampleExchange:
|
|
workflows.sa2se(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.RobotSampleExchange:
|
|
workflows.sa2rse(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.DataCollection:
|
|
workflows.sa2dc(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.XrayFluorescence:
|
|
workflows.sa2xrf(self.__devs, self.__cfg)
|
|
elif target == BeamlineStateEnum.BeamLocation:
|
|
workflows.sa2bl(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.XrayFluorescence:
|
|
if target == BeamlineStateEnum.SampleAlignment:
|
|
workflows.xrf2sa(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
case BeamlineStateEnum.RobotSampleExchange:
|
|
if target == BeamlineStateEnum.SampleAlignment:
|
|
workflows.rse2sa(self.__devs, self.__cfg)
|
|
else:
|
|
raise TransformationInvalidException()
|
|
self.__cfg.state = target
|
|
except TransformationInvalidException as e:
|
|
self.__cfg.state = curr_state
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
except Exception as e:
|
|
self.__cfg.state = BeamlineStateEnum.Maintenance
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
|
|
@property
|
|
def shutter(self) -> bool:
|
|
return self.__devs.shutter
|
|
|
|
@shutter.setter
|
|
def shutter(self, v: bool):
|
|
self.__devs.shutter = v
|
|
|
|
@property
|
|
def diffraction_geometry(self) -> DiffractionGeometry:
|
|
det_cfg = self.__jfjoch.detector()
|
|
return DiffractionGeometry(
|
|
energy_keV=self.__devs.energy_kev,
|
|
dtz_mm=self.__devs.dtz.value,
|
|
detector_size_pxl=(det_cfg.width, det_cfg.height),
|
|
pixel_size_mm=det_cfg.pixel_size_mm,
|
|
beam_center_pxl=self.__cfg.beam_center,
|
|
detector_description=det_cfg.description,
|
|
detector_serial_number=det_cfg.serial_number,
|
|
poni_rot1_rad=-0.001396263,
|
|
poni_rot2_rad=-0.003839724,
|
|
)
|
|
|
|
@property
|
|
def beamline_status(self) -> BeamlineStatus:
|
|
return BeamlineStatus(
|
|
ring_current_mA=self.__devs.ring_current,
|
|
light=self.light,
|
|
cryojet_K=self.__devs.cryojet_temp,
|
|
shutter_open=self.__devs.shutter,
|
|
exp_shutter_open=self.__devs.exp_shutter.state(),
|
|
flux_ph_s=self.__devs.full_flux,
|
|
sample_camera=self.__devs.samcam_settings,
|
|
name=self.__bl,
|
|
transmission=self.__devs.transmission.get(),
|
|
zoom=self.__devs.zoom,
|
|
commissioning_mode=self.__cfg.commissioning_mode,
|
|
dtz_min=self.__devs.dtz_low,
|
|
dtz_max=self.__devs.dtz_high,
|
|
)
|
|
|
|
@property
|
|
def status(self) -> DAQStatusModel:
|
|
# session should be set by FastAPI server
|
|
return DAQStatusModel(
|
|
state=self.state,
|
|
busy=self.busy,
|
|
geom=self.sample_geometry,
|
|
bl=self.beamline_status,
|
|
sample=self.sample,
|
|
session=SessionStatus(),
|
|
diffraction=self.diffraction_geometry,
|
|
box=self.__saved_box,
|
|
last_best_res = self.__cfg.last_best_res,
|
|
last_best_b_factor = self.__cfg.last_best_b_factor,
|
|
crystal_size = self.__cfg.crystal_size
|
|
)
|
|
|
|
def cancel(self):
|
|
if self.__cfg.state == BeamlineStateEnum.DataCollection:
|
|
self.__devs.aerotech.stop()
|
|
self.__jfjoch.cancel()
|
|
|
|
def anneal(self, time_s: float):
|
|
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
|
|
try:
|
|
self.__devs.anneal(time_s)
|
|
finally:
|
|
self.__cfg.state_busy = False
|
|
|
|
def fluorimeter_take_spectrum(self, fm: FluorescenceSpectrumParameterModel) -> FluorescenceSpectrumOutputModel:
|
|
self.__cfg.try_set_busy(timeout=360)
|
|
|
|
try:
|
|
self.__set_state(BeamlineStateEnum.XrayFluorescence)
|
|
|
|
if fm.transmission is not None:
|
|
self.__devs.transmission.set(fm.transmission, wait=True)
|
|
|
|
self.__devs.fluorimeter.start_acquisition(erase=fm.erase)
|
|
self.__devs.aerotech.set_shutter(1)
|
|
time.sleep(fm.acq_time_s)
|
|
self.__devs.aerotech.set_shutter(0)
|
|
self.__devs.fluorimeter.stop_acquisition()
|
|
time.sleep(0.2)
|
|
spectrum = self.__devs.fluorimeter.get_current_data()
|
|
|
|
offset = self.__devs.fluorimeter.offset
|
|
slope = self.__devs.fluorimeter.slope # slope is in keV!
|
|
energy = [slope * 1000.0 * i + offset for i in range(len(spectrum))]
|
|
fluo_output = FluorescenceSpectrumOutputModel(spectrum=spectrum,
|
|
bkg=self.__devs.fluorimeter.get_current_background(),
|
|
energy_eV=energy,
|
|
average_dead_time=self.__devs.fluorimeter.average_dead_time() / 100.0)
|
|
if self.sample is not None:
|
|
self.__cfg.xrf = fluo_output
|
|
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
return fluo_output
|
|
except Exception as e:
|
|
self.__set_state(BeamlineStateEnum.SampleAlignment)
|
|
self.__cfg.state_busy = False
|
|
raise e
|
|
|
|
def fluorimeter_start(self, erase: bool = False):
|
|
self.__devs.aerotech.set_shutter(1)
|
|
self.__devs.fluorimeter.start_acquisition(erase=erase)
|
|
|
|
def fluorimeter_stop(self):
|
|
self.__devs.fluorimeter.stop_acquisition()
|
|
self.__devs.aerotech.set_shutter(0)
|
|
|
|
def fluorimeter_status(self) -> int | None:
|
|
return self.__devs.fluorimeter.check_status()
|
|
|
|
def fluorimeter_data(self):
|
|
return self.__devs.fluorimeter.get_current_data()
|
|
|
|
def fluorimeter_background(self):
|
|
return self.__devs.fluorimeter.get_current_background() |